Source code for acia.tracking.processor.utils

"""Utility functions for the tracking processors"""

from __future__ import annotations

from collections import defaultdict

import numpy as np

from acia.base import Overlay
from acia.segm.rasterize import contour_labels, frame_label_mask


[docs] def overlay_to_masks(segmentation: Overlay, height: int, width: int) -> np.ndarray: """Rasterize an overlay into the ``(T, height, width)`` label stack trackers want. Every tracking backend takes its segmentation this way (Trackastra, LapTrack, PyUAT, Ultrack), so this is on the critical path of each of them. Frame ``t`` of the returned stack holds the detections of frame ``t`` of the overlay -- **indexed absolutely**. The previous implementation built the stack from ``Overlay.timeIterator()``, which starts at the overlay's *first populated* frame, so an overlay whose earliest detection sat on frame 3 put that frame's cells into ``masks[0]`` and handed the tracker a segmentation silently shifted against its images. Args: segmentation (Overlay): the detections to rasterize. height (int): frame height, in pixels. width (int): frame width, in pixels. Returns: np.ndarray: ``(T, height, width)`` label stack, 0 = background. ``uint16``, widened to ``uint32`` if any label needs it. """ # one pass over the contours instead of the nested timeIterator() the old # implementation ran (once here, then again inside Overlay.toMasks for every # single-frame sub-overlay, rebuilding an object array and a lookup dict # each time) by_frame: dict[int, list] = defaultdict(list) for cont in segmentation: by_frame[cont.frame].append(cont) frames = segmentation.frames() num_frames = int(np.max(frames)) + 1 if len(frames) else 0 if by_frame: # a detection outside the overlay's declared frame list still has to fit num_frames = max(num_frames, int(max(by_frame)) + 1) # resolve the labels up front so the buffer's dtype is decided before it is # allocated: labels past 65535 must widen the stack rather than wrap in it labels = { frame: contour_labels(conts, enumerate_fallback=True) for frame, conts in by_frame.items() } max_label = max((max(ls) for ls in labels.values() if ls), default=0) dtype = np.uint16 if max_label < np.iinfo(np.uint16).max else np.uint32 # preallocated rather than np.stack()ed from a list, which would hold the # per-frame masks and their stacked copy at the same time masks = np.zeros((num_frames, height, width), dtype=dtype) for frame, conts in by_frame.items(): masks[frame] = frame_label_mask( conts, height=height, width=width, labels=labels[frame], # trackers associate on cell geometry, so the polygons have to # rasterize exactly as they always did -- cv2.fillPoly would dilate # every cell by a pixel exact_polygons=True, ) return masks