Source code for acia.segm.processor.flowpose_rt

"""flowpose-rt segmentation implementation"""

import gc

import numpy as np
from tqdm.auto import tqdm

from acia.attribute import attribute_segmentation
from acia.base import ImageSequenceSource, Overlay
from acia.segm.formats import overlay_from_masks

from . import SegmentationProcessor


def _batch(iterable, n=1):
    length = len(iterable)
    for ndx in range(0, length, n):
        yield iterable[ndx : min(ndx + n, length)]


[docs] class FlowposeRTSegmenter(SegmentationProcessor): """flowpose-rt segmentation implementation (omnipose-compatible, lighter deps). ``batch_size`` trades memory for throughput: ``flowpose_rt.Segmenter.segment()`` only tiles a genuinely single (H, W) image internally -- a stacked (N, H, W) batch (what a ``batch_size``-chunked call sends) skips that tiling and forwards the whole chunk through the network at full resolution, so a larger batch of frames bigger than flowpose-rt's ~224px tile size costs more memory per image than single-frame calls would. Lower ``batch_size`` if memory is a concern for large frames. ``weights_path`` selects a local checkpoint instead of the downloaded zoo weights (e.g. a fine-tuned model). ``model`` is still required in that case: it names the zoo entry whose preprocessing contract (channel count, channel mapping, default tolerance) the checkpoint follows. """
[docs] def __init__( self, model="bact_phase_omni", device="auto", precision="auto", compile=None, # noqa: A002 - mirrors flowpose_rt.Segmenter's own kwarg name autorelease: bool = True, batch_size: int = 20, weights_path=None, ): super().__init__(autorelease=autorelease) self.model_spec = model self.device = device self.precision = precision self.compile = compile self.batch_size = batch_size self.weights_path = weights_path
def _load_model(self): import flowpose_rt as ort if self.weights_path is not None: # from_path never downloads and never checksums; model_spec supplies # the preprocessing contract the local checkpoint is assumed to match return ort.Segmenter.from_path( self.weights_path, model_type=self.model_spec, device=self.device, precision=self.precision, compile=self.compile, ) return ort.Segmenter( model=self.model_spec, device=self.device, precision=self.precision, compile=self.compile, ) def _release_model(self) -> None: super()._release_model() try: import torch if torch.cuda.is_available(): # torch.compile(mode="reduce-overhead") (flowpose_rt's default on # CUDA) caches CUDA-graph memory pools process-globally, not # scoped to the model instance -- dropping the model reference # and torch.cuda.empty_cache() (in the base class) don't reclaim # those pools, so reset the compile cache explicitly here. torch.compiler.reset() except Exception: pass def _segment(self, images: ImageSequenceSource) -> Overlay: imgs = [] for image in images: raw_image = image.raw # Reduce HxWxC=1 image to HxW shape if len(raw_image.shape) == 3: if raw_image.shape[2] != 1: raise ValueError( f"FlowposeRTSegmenter only accepts a single channel image. Currently it is HxWxC: {raw_image.shape}" ) # make it a grayscale image raw_image = raw_image[..., 0] imgs.append(raw_image) # one vectorized net forward per batch (not per image) -- batch_size # trades progress granularity for throughput; a batch of 1 shows # per-image progress but loses flowpose-rt's batched-forward speedup all_masks = [] pbar = tqdm(total=len(imgs), desc="Batched flowpose-rt prediction...") for image_batch in _batch(imgs, self.batch_size): stack = np.stack(image_batch) all_masks.append(self.model.segment(stack)) pbar.update(len(image_batch)) # encourage prompt reclamation of this batch's activation memory # before the next batch starts, rather than only at end-of-call gc.collect() masks = np.concatenate(all_masks) ov = overlay_from_masks(masks) attribute_segmentation(ov, self) return ov