spacr.qt.widgets.timelapse_preview

Timelapse live preview — segment once, re-link live.

The Timelapse module is mask generation over a time series followed by frame-to-frame linking. Those two halves cost wildly different amounts: segmenting twelve frames with Cellpose is tens of seconds, re-linking the same twelve label images with a new timelapse_displacement is milliseconds. A preview that re-segments on every slider move is unusable, so this panel splits them:

  • Per-frame masks are cached under a segmentation signature — the source path, the frame indices, and every setting that can change a label image (model, channel, diameter, flow threshold, cell probability, normalisation). Change a tracking setting and the signature is unchanged, the cache hits, and only link_tracks() runs. Change a segmentation setting and the signature moves, so the masks are rebuilt.

  • The sequence is read lazily. FrameSequence never materialises the whole time series: a directory reads one file at a time, a multi-page TIFF reads one page at a time, and an .npy stack is memory-mapped and sliced. Only the frames the preview actually shows are touched, and a small LRU keeps the scrubber responsive without pinning the movie in RAM.

What the panel shows is chosen around the two failure modes people actually tune a tracker against:

  • Fragmentation — one object becoming several track ids. Surfaced as track count, mean/median track length, the number of tracks shorter than a live-settable N, and the count of tracks that start after the first frame or end before the last one.

  • Identity swaps — a track jumping to a different object. Surfaced as the number of within-track steps longer than the displacement limit the user is tuning, plus the largest single step. Masks are relabelled by track id and drawn in a per-track colour, so a swap is visible as an object changing colour mid-movie.

Both indicators are computed without ground truth and are labelled in the UI as indicators, not measurements.

Optional backends (trackastra, ultrack) are detected before they are called: absent, the panel writes one inline line naming the package and the install command. An ImportError traceback never reaches the user.

Exceptions

MovieFieldCancelled

A queued movie field was abandoned before it retained its arrays.

TrackerUnavailable

A linking backend cannot run here, with an actionable reason.

Classes

FrameSequence

A time series read one frame at a time.

TimelapsePreviewPanel

Interactive tracking preview — Timelapse module.

TimelapseRequest

One preview pass. cached_masks is what makes re-linking cheap.

TrackStats

What the user is actually tuning against.

Functions

backend_available(→ Tuple[bool, str])

Whether linking backend mode can run, and why not when it cannot.

build_movie_field(→ Dict[str, Any])

Open, segment and link one additional field without touching Qt UI.

build_timelapse_preview_card(host, *[, panel_later])

Build the Track preview card + panel pair.

frame_channel(→ numpy.ndarray)

Return a 2-D plane from a frame stored either (H, W, C) or (C, H, W).

link_tracks(masks[, mode, displacement, memory, ...])

Link a (T, H, W) label stack into tracks with the chosen backend.

movie_field_payload(→ Dict[str, Any])

Never let one bad sibling strand the remaining movie-field queue.

movie_worker_interrupted(→ bool)

Whether the JobRunner thread executing this field was cancelled.

open_sequence_payload(→ Dict[str, Any])

Open a sequence and list its neighbours. No Qt, so it runs on a worker.

relabel_by_track(→ numpy.ndarray)

Recolour a label stack by track id using spaCR's own relabeller.

render_frame(→ numpy.ndarray)

Render one preview frame: image, mask outlines, and track history.

run_preview_pass(→ Dict[str, Any])

Do the work of one preview: masks (maybe cached), then linking.

segment_frame(→ numpy.ndarray)

Segment one frame with Cellpose and return an int32 label image.

segment_sequence(→ numpy.ndarray)

Segment every preview frame of seq into a (T, H, W) label stack.

track_colour(→ Tuple[int, int, int])

Deterministic colour for a track id, stable across frames and runs.

track_stats(→ TrackStats)

Summarise a track table into the numbers that drive a tuning decision.

Module Contents

exception spacr.qt.widgets.timelapse_preview.MovieFieldCancelled[source]

Bases: RuntimeError

A queued movie field was abandoned before it retained its arrays.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.qt.widgets.timelapse_preview.TrackerUnavailable[source]

Bases: RuntimeError

A linking backend cannot run here, with an actionable reason.

Raised instead of letting an ImportError escape so the panel can put the message inline. The string always names the package and the command that fixes it.

Initialize self. See help(type(self)) for accurate signature.

class spacr.qt.widgets.timelapse_preview.FrameSequence(kind: str, source, n_available: int, indices: Sequence[int], label: str = '', cache_size: int = 6)[source]

A time series read one frame at a time.

Three layouts are understood, and none of them is ever read whole:

  • a directory of per-frame image files — one file opened per access;

  • a multi-page TIFF — one page decoded per access (tifffile.imread(path, key=i));

  • an .npy stack whose first axis is time — memory-mapped, so a slice touches only that plane’s pages.

Parameters:
  • kind – "files", "tiff" or "npy".

  • source – the list of paths (files) or the single path.

  • n_available – how many frames exist on disk.

  • indices – the subset of frame indices this sequence exposes, so a 400-frame movie can be previewed as its first 12 frames.

  • label – what to call the sequence in the UI. Empty falls back to str(source), which for a file list is the list – fine for one path, unreadable for four hundred, so anything user-facing should pass a name.

  • cache_size – how many decoded frames to keep in the LRU.

Variables:

read_count – number of decodes actually performed — the instrument the tests assert against to prove nothing is read eagerly.

Hold one sequence of frames, read lazily and cached.

Parameters:
  • kind – what the frames are – images, masks, or an overlay.

  • source – where to read them from.

  • n_available – how many frames exist.

  • indices – which of them this sequence shows.

  • label – the caption for this sequence.

  • cache_size – how many decoded frames to keep.

__len__() → int[source]

How many frames this sequence shows.

Returns:

the frame count.

describe() → str[source]

One-line summary for the status label.

frame(i: int) → numpy.ndarray[source]

Return preview-frame i (0-based over indices).

Parameters:

i – preview-frame position; outside 0 to len(indices) - 1 raises IndexError. Frames read are kept in a small cache.

classmethod open(path, max_frames: int = 12) → FrameSequence[source]

Open path as a sequence, reading at most metadata to do so.

Parameters:
  • path – a directory of frames, a multi-page TIFF, or an .npy stack whose first axis is time.

  • max_frames – cap on the number of frames the preview exposes.

Raises:
property truncated: bool[source]

Whether the preview is showing fewer frames than exist on disk.

class spacr.qt.widgets.timelapse_preview.TimelapsePreviewPanel(parent=None, *, threaded: bool = True)[source]

Bases: spacr.qt.widgets.preview_contract.LivePreviewContract, PySide6.QtWidgets.QWidget

Interactive tracking preview — Timelapse module.

Same contract as LivePreviewPanel, and now literally the same code for the shared half: a standalone QWidget, a QThread worker that emits results over signals, LivePreviewContract for the run/cancel/status protocol, set_propagate_callback() to push tuned values back into the main settings panel, and a build_*_card factory.

Parameters:
  • parent – parent widget.

  • threaded – whether the jobs run off the GUI thread. Opening a sequence reads a TIFF header or memory-maps a stack and then lists every sibling field of view, which is not GUI-thread work on a plate. False runs each job inline, emitting the same signals in the same order, so a test can drive this panel synchronously without the behaviour diverging.

Build the preview: its canvases, its scrub bar and its controls.

Parameters:
  • parent – parent widget.

  • threaded – whether work runs on a worker.

apply_settings(settings: dict) → None[source]

Seed the preview from the main Timelapse settings dict.

Parameters:

settings – the Timelapse settings; a copy is kept, and the linking mode, displacement, memory, object, transient filter and that object’s channel, diameter and model are read from it.

attach_movie_panel(movie) → None[source]

Wire a TimelapseMoviePanel to this preview.

Kept as a seam rather than a constructor argument so the movie is optional: the panel is built by two different callers and a screen that only wants the stats view should not pay for the frames.

Parameters:

movie – the movie panel; its max_fields_changed signal is connected and a previously attached panel is disconnected.

closeEvent(event)[source]

Let a running pass finish before the widget is torn down.

A QThread collected while it is still running aborts the whole process, and this panel’s worker outlives the emit that produced its result by a few instructions.

Parameters:

event – the close event; passed on to the base class after the workers finish (up to 5 s each).

current_params() → dict[source]

Snapshot for tests + external callers.

display_channel() → int | None[source]

Channel index the canvases show, or None when unset.

dragEnterEvent(event)[source]

Accept a drag carrying a timelapse folder or one of its frames.

Parameters:

event – the Qt drag event.

dragMoveEvent(event)[source]

Keep accepting while a timelapse folder or one of its frames stays over the panel.

Parameters:

event – the Qt drag event.

dropEvent(event)[source]

Take the dropped input and preview it.

Parameters:

event – the Qt drop event.

load_masks(path) → bool[source]

Synchronously use ready-made label images instead of segmenting.

For programmatic callers and tests. The GUI uses load_masks_async(), because this one opens the folder on the thread that calls it.

Parameters:

path – the label images: a directory of frames, a multi-page TIFF, or an .npy stack whose first axis is time.

Returns:

False, with the reason in the status line, when it cannot be opened.

load_masks_async(path) → bool[source]

Open path as a mask sequence on a worker, then install it here.

THE GUI ENTRY POINT, and the reason it exists is that load_masks() opens the sequence inline. FrameSequence.open on a folder of label images is a stat, a listing and one is_file() per entry – hundreds of round trips for a plate – and the folder comes from the user, which on one such workstation means it can be a sleeping /nas_mnt share where a single stat had not returned after twenty seconds (measured; see spacr.qt.path_probe). Run from _pick_masks() that froze the whole window the moment the file dialog closed.

The same worker function as the image sequence, so the mask sequence’s first frame is warmed off the GUI thread too.

Parameters:

path – the label images: a directory of frames, a multi-page TIFF, or an .npy stack whose first axis is time. None or empty submits nothing.

Returns:

True when a job was submitted.

load_sequence(path) → bool[source]

Synchronously open path as the preview sequence.

For programmatic callers and tests, mirroring LivePreviewPanel.load_image. The GUI uses load_sequence_async().

Parameters:

path – the image sequence: a directory of frames, a multi-page TIFF, or an .npy stack whose first axis is time.

Returns:

False, with the reason in the status line, when it cannot be opened.

load_sequence_async(path, *, list_siblings: bool = True) → bool[source]

Open path on a worker, then install it on the GUI thread.

Every GUI entry point – the drop handler, the Choose-sequence dialog and the FOV dropdown – comes through here.

Parameters:
  • path – the image sequence: a directory of frames, a multi-page TIFF, or an .npy stack whose first axis is time. None or empty submits nothing.

  • list_siblings – list the sibling fields next to path for the field dropdown; False reuses the cached listing.

Returns:

True when a job was submitted.

propagate_settings() → None[source]

Push the current settings to the main panel, if wired.

refresh_model_choices() → None[source]

Re-read the Cellpose model list and add anything new.

spacr.settings.cellpose_model_choices only reads the API when Cellpose is already imported, because importing it costs ~2.5 s and this panel is built while a page is being laid out. That means the first build usually gets the shipped fallback — so ask again every time the panel is shown. After the first segmentation Cellpose is loaded and a checkpoint the user registered appears here.

Additive on purpose: the current selection is never disturbed, and an entry is never removed, so a value the user picked cannot vanish under them because a probe came back thinner.

Re-link the cached masks. Never re-segments.

run_preview() → None[source]

Segment (unless cached) then link, off the GUI thread.

sample_note() → str[source]

The sentence stating this preview is a sample of N of M sets.

set_propagate_callback(cb) → None[source]

Register a callback(dict) that writes tuned values into the main settings panel (wired by the AppScreen).

Parameters:

cb – the callable, or None to remove it.

settings_for_propagation() → dict[source]

Map the preview’s widgets onto real Timelapse setting keys.

showEvent(event)[source]

Refresh the model list whenever the panel comes back on screen.

Parameters:

event – the show event; passed on to the base class first.

shutdown() → None[source]

Abandon anything in flight and leave no QThread behind.

class spacr.qt.widgets.timelapse_preview.TimelapseRequest[source]

One preview pass. cached_masks is what makes re-linking cheap.

class spacr.qt.widgets.timelapse_preview.TrackStats[source]

What the user is actually tuning against.

fragmentation_* and suspicious_jumps are indicators computed without ground truth, not measurements — the panel labels them as such.

summary() → str[source]

The one-line status the panel pins under the canvas.

property fragmentation_events: int[source]

Track starts after frame 0 plus track ends before the last frame.

spacr.qt.widgets.timelapse_preview.backend_available(mode: str) → Tuple[bool, str][source]

Whether linking backend mode can run, and why not when it cannot.

Checks with importlib.util.find_spec(), so nothing heavy is imported just to answer the question and a missing optional dependency never raises.

Parameters:

mode – linking backend name, one of TRACK_MODES (case-insensitive); iou needs no optional package.

Returns:

(True, "") when usable, else (False, message) where the message names the package and the command that installs it.

spacr.qt.widgets.timelapse_preview.build_movie_field(path, *, max_frames: int, seg: Dict[str, Any], track: Dict[str, Any], cached_masks: numpy.ndarray | None = None, cancelled: Callable[[], bool] | None = None) → Dict[str, Any][source]

Open, segment and link one additional field without touching Qt UI.

Raw frames are retained for the movie, so a cache miss reads each frame exactly once and hands that same array to segmentation. cancelled is checked between frames and before linking; the production callback reads the worker QThread’s interruption flag, which lets lowering the Fields cap stop an expensive sibling before it retains the rest of the sequence.

Parameters:
  • path – the field to open: a directory of frames, a multi-page TIFF, or an .npy stack whose first axis is time.

  • max_frames – cap on the number of frames read.

  • seg – segmentation parameters, as segment_frame() takes them; its channel is also returned.

  • track – keyword arguments for link_tracks().

  • cached_masks – an existing (T, H, W) label stack to reuse instead of segmenting; its frame count must match the sequence.

  • cancelled – a no-argument callable returning True to stop.

spacr.qt.widgets.timelapse_preview.build_timelapse_preview_card(host, *, panel_later: bool = False)[source]

Build the Track preview card + panel pair.

Mirrors spacr.qt.screens.hyperparam.build_hyperparam_card and app_screen._build_live_preview_card: returns the pair without adding it to any layout, so the host screen puts it in whatever splitter it likes and starts it hidden behind the toggle.

Parameters:
  • host – the AppScreen asking for the card.

  • panel_later – return None for the panel and leave the card empty, for _fill_timelapse_preview_card() to fill the first time it is shown – how Mask carries this preview for its folded Timelapse switch without building ~130 widgets nobody has asked to see.

Returns:

(panel, card).

spacr.qt.widgets.timelapse_preview.frame_channel(frame: numpy.ndarray, channel: int) → numpy.ndarray[source]

Return a 2-D plane from a frame stored either (H, W, C) or (C, H, W).

A merged frame written by spaCR is channel-last, but a raw acquisition TIFF page is often channel-first. Guessing wrong turns a 3-channel image into a 3-pixel-tall one, so pick the axis that actually looks like a channel axis (small, and not the only small axis).

Parameters:
  • frame – the frame array; a 2-D frame is returned as is and one with other than three dimensions is squeezed.

  • channel – the channel index to take (wrapped modulo the channel count on a channel-first frame).

Link a (T, H, W) label stack into tracks with the chosen backend.

Parameters:
  • masks – label stack with at least two frames; any other shape raises ValueError.

  • mode – linking backend, one of TRACK_MODES.

  • displacement – maximum displacement between frames, in pixels, passed to the trackpy, btrack and ultrack backends.

  • memory – frames an object may vanish for (trackpy only).

  • iou_threshold – minimum overlap to link (iou only).

  • images – intensity stack for trackastra.

  • trackastra_model – trackastra model name.

  • trackastra_linking – trackastra linking mode.

Returns:

a DataFrame with frame, original_label, track_id, x and y — the same layout the pipeline’s trackers emit.

Raises:

TrackerUnavailable – when the backend’s package is missing, with a message naming the package and the install command.

spacr.qt.widgets.timelapse_preview.movie_field_payload(**kwargs) → Dict[str, Any][source]

Never let one bad sibling strand the remaining movie-field queue.

spacr.qt.widgets.timelapse_preview.movie_worker_interrupted() → bool[source]

Whether the JobRunner thread executing this field was cancelled.

spacr.qt.widgets.timelapse_preview.open_sequence_payload(path, max_frames: int = 12, list_siblings: bool = True) → Dict[str, Any][source]

Open a sequence and list its neighbours. No Qt, so it runs on a worker.

Warms the sequence’s own frame cache with frame 0, because _frame_channel_count decodes exactly that frame on the GUI thread to fill the channel dropdown – doing it here turns that read into a cache hit rather than a second trip to disk.

Parameters:
  • path – sequence file or frame directory accepted by FrameSequence.open(); the same value is returned as text in the payload even when opening fails.

  • list_siblings – False reuses the sampler’s cached listing; the FOV dropdown hands out a path it has already enumerated.

Returns:

{path, sequence, siblings, error}. When list_siblings is true siblings is ALWAYS a list, never None, even if the listing failed – see the fallback below for why that matters.

spacr.qt.widgets.timelapse_preview.relabel_by_track(masks: numpy.ndarray, tracks) → numpy.ndarray[source]

Recolour a label stack by track id using spaCR’s own relabeller.

This is what makes an identity swap visible: an object that keeps its track id keeps its colour across frames, and one that is handed to another track changes colour mid-movie.

Parameters:
  • masks – the (T, H, W) label stack the tracks were built from.

  • tracks – the track table from link_tracks(); None or an empty table gives an all-zero stack.

spacr.qt.widgets.timelapse_preview.render_frame(image: numpy.ndarray, labels: numpy.ndarray | None = None, tracks=None, frame: int = 0, tail: int = 12, normalise: bool = True, lo_pct: float = 2.0, hi_pct: float = 98.0, channel: int = 0) → numpy.ndarray[source]

Render one preview frame: image, mask outlines, and track history.

Outlines are coloured per track id so a fragmented track shows up as an object that changes colour partway through, and the trailing polyline shows where each object came from over the last tail frames.

Parameters:
  • image – the frame to draw, 2-D or with a channel axis; the plane is picked with frame_channel().

  • labels – optional track-relabelled mask for this frame, outlined per track colour.

  • tracks – optional track table with x, y, frame and track_id; trails are drawn up to frame.

  • frame – index of this frame in the track table.

  • tail – how many earlier frames each trail reaches back.

  • normalise – stretch the plane between the two percentiles.

  • lo_pct – lower percentile of the stretch.

  • hi_pct – upper percentile of the stretch.

  • channel – channel of image to draw.

Returns:

an RGB uint8 array.

spacr.qt.widgets.timelapse_preview.run_preview_pass(req: TimelapseRequest) → Dict[str, Any][source]

Do the work of one preview: masks (maybe cached), then linking.

Parameters:

req – the request; its cached masks are used first, then its mask sequence, then its image sequence is segmented with req.seg, and the masks are linked with req.track. With none of these it raises ValueError.

spacr.qt.widgets.timelapse_preview.segment_frame(image: numpy.ndarray, params: Dict[str, Any]) → numpy.ndarray[source]

Segment one frame with Cellpose and return an int32 label image.

Cellpose is imported inside the call so importing this module — as the test suite does — costs nothing and needs no CUDA stack. The panel calls this through the module global, which is also what lets a test swap in a counting stub to prove that tuning a tracking setting never reaches segmentation.

Parameters:
  • image – the frame, 2-D or with a channel axis.

  • params – segmentation settings; model, channel, normalise, lo_pct, hi_pct, diameter, flow_threshold and cellprob are read, each with a default. A cellpose3:... model is segmented in the Cellpose 3 backend by spacr.object._cellpose3_masks(), as the run segments it, and a cellpose_dino:<path> model in the Cellpose-DINO backend by spacr.object._cellpose_dino_masks().

spacr.qt.widgets.timelapse_preview.segment_sequence(seq: FrameSequence, params: Dict[str, Any]) → numpy.ndarray[source]

Segment every preview frame of seq into a (T, H, W) label stack.

Reads and segments one frame at a time so the whole movie is never resident, then stacks only the label images (which are far smaller than the raw multi-channel frames).

Parameters:
  • seq – the frame sequence to segment.

  • params – segmentation settings, as segment_frame() takes them.

spacr.qt.widgets.timelapse_preview.track_colour(track_id: int) → Tuple[int, int, int][source]

Deterministic colour for a track id, stable across frames and runs.

Parameters:

track_id – the track id, converted with int; it indexes TRACK_COLOURS cyclically.

spacr.qt.widgets.timelapse_preview.track_stats(tracks, n_frames: int, min_length: int = 3, displacement_limit: float = 50.0) → TrackStats[source]

Summarise a track table into the numbers that drive a tuning decision.

Parameters:
  • tracks – DataFrame with frame, track_id and (for the swap indicator) x/y.

  • n_frames – how many frames the preview covered.

  • min_length – tracks shorter than this are counted as short — the live-settable threshold that says what “too short” means here.

  • displacement_limit – the linking radius being tuned; a within-track step longer than this is counted as a swap risk.