Source code for phenotypic.abc_._gpu_detector

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, List

import numpy as np

from phenotypic.sdk_.typing_ import GpuInputLayer, GpuOutputKind

if TYPE_CHECKING:
    from phenotypic._core._image import Image

from ._object_detector import ObjectDetector


# <<Interface>>
[docs] class GpuDetector(ObjectDetector, ABC): """Interface ABC for GPU-accelerated object detectors (batched/streaming). Subclass GpuDetector when your detection algorithm depends on a GPU (e.g., deep-learning foundation models like SAM2 or micro-sam). GpuDetector provides a concrete ``_operate`` built from a small set of overridable hooks — ``preprocess`` (raw ``input_layer`` array → model-ready sample), ``collate`` (samples → batch), ``infer_batch`` (batch → per-sample results), and ``_write_object_output`` (result → ``objmap``/``objmask``). The single-image notebook path and the batched CLI engine drive the *same* hooks, so a detector implemented once runs in both. Capability is declared via three fields — ``input_layer`` (``rgb``/``gray``/``detect_mat``), ``supports_batching``, and ``output_kind`` (``instance``/``semantic``). When a pipeline contains a GpuDetector, the CLI enforces: - **Local execution:** Sequential processing (n_jobs=1) to avoid multiple workers competing for the same GPU. - **SLURM execution:** Automatically requests GPU resources (``--gpus-per-node=1``) if the user hasn't specified GPU args. Raises an error if the target partition has no GPUs. - **No GPU available:** Raises RuntimeError at pipeline validation time with a clear message. **When to subclass GpuDetector vs ObjectDetector** Subclass GpuDetector if your detector relies on GPU-accelerated inference: - Deep-learning models (SAM2, micro-sam, or custom neural networks). - Any algorithm that requires ``torch``, ``tensorflow``, or similar GPU-backed frameworks at inference time. - Detectors where CPU fallback is technically possible but impractically slow for production use. Subclass ObjectDetector directly if your algorithm is CPU-based: - Classical computer vision (thresholding, edge detection, watershed). - Algorithms implemented with NumPy, SciPy, or scikit-image. - Detectors that run in milliseconds on CPU. **Lazy model loading** GpuDetector subclasses should defer model construction to the first ``apply()`` call rather than ``__init__()``. This enables: - Fast construction and serialization round-trips without GPU/torch. - Pipeline ``to_json()``/``from_json()`` without importing heavy dependencies. - Parameter inspection and validation before committing GPU memory. Use a ``_ensure_model_loaded()`` pattern:: from pydantic import PrivateAttr class MyGpuDetector(GpuDetector): model_size: str = "small" # Annotated class-level fields device: str = "auto" # underscore-prefixed private attr → skipped by serialization _model: object = PrivateAttr(default=None) def _ensure_model_loaded(self): if self._model is not None: return import torch # lazy import # ... build model ... def _infer_one(self, sample): # ``sample`` is a preprocessed (H, W, 3) array. Return a uint16 # labeled objmap (output_kind="instance") or a bool mask # (output_kind="semantic"). The base _operate/infer_batch wire # this into the image; do NOT override _operate. # ... run inference ... return objmap Notes: ``_operate`` is concrete here and should not be overridden. Non-batchable subclasses (SAM2, micro-sam) implement just ``_ensure_model_loaded`` + ``_infer_one``; the default ``infer_batch`` loops ``_infer_one`` and is the sole caller of ``_ensure_model_loaded``. Batchable subclasses (Spec 2 foundation models) instead override ``infer_batch`` with a true ``(N, C, H, W)`` forward — no engine changes needed. The class also lets the CLI make informed GPU resource-allocation decisions. """ # Capability / routing markers — pydantic FIELDS (not ClassVar) so they # serialize and round-trip (Spec 1 §4, review S4). Subclasses override the # defaults; "instance" keeps existing SAM behavior unchanged. input_layer: GpuInputLayer = "rgb" supports_batching: bool = False output_kind: GpuOutputKind = "instance" @abstractmethod def _ensure_model_loaded(self) -> None: """Build/load the GPU model on first use (idempotent)."""
[docs] def preprocess(self, array: np.ndarray) -> Any: """Turn a raw ``input_layer`` array into a model-ready sample (CPU). Default: a single-channel 2D layer (``gray``/``detect_mat``) is stacked into an ``(H, W, 3)`` block so 3-channel models (SAM/DINO ViT) consume it unchanged; ``rgb`` passes through untouched. Subclasses may override for model-specific normalization (e.g. uint8 coercion). """ if array.ndim == 2: return np.stack([array, array, array], axis=-1) return array
[docs] def collate(self, samples: List[Any]) -> Any: """Merge per-sample ``preprocess`` outputs into a batch. Default returns the list unchanged (consumed by the looped ``infer_batch``). Batchable subclasses override to stack into a tensor. """ return samples
[docs] def infer_batch(self, batch: Any) -> List[np.ndarray]: """Run inference over a collated batch; return one result per sample. Each result is a uint16 labeled map (``output_kind="instance"``) or a boolean mask (``output_kind="semantic"``). The default loops ``_infer_one`` (correct for ``supports_batching=False``); batchable subclasses override with a true ``(N, C, H, W)`` forward. """ self._ensure_model_loaded() return [self._infer_one(sample) for sample in batch]
def _infer_one(self, sample: Any) -> np.ndarray: """Run the model on ONE preprocessed sample. Subclasses must implement. Returns a uint16 labeled objmap (instance) or a boolean mask (semantic). """ raise NotImplementedError( f"{type(self).__name__} must implement _infer_one()" ) def _write_object_output(self, image: "Image", result: np.ndarray) -> None: """Write one ``infer_batch`` result onto the image per ``output_kind``. - ``instance`` -> ``image.objmap[:]`` (detector-controlled labels). - ``semantic`` -> ``image.objmask[:]`` (auto-labels into the shared ``objmap`` backend, exactly like a threshold detector; see Spec 1 §8). """ if self.output_kind == "instance": image.objmap[:] = result.astype(np.uint16) else: # semantic image.objmask[:] = result.astype(bool) def _operate(self, image: "Image") -> "Image": """Run GPU detection on one image (notebook / single-image path). Reads the declared ``input_layer``, preprocesses, runs a one-element batch through ``collate`` + ``infer_batch``, and writes the result via ``output_kind``. The batched CLI engine drives the same ``preprocess``/``collate``/``infer_batch`` methods over many images. """ array = getattr(image, self.input_layer)[:] sample = self.preprocess(array) batch = self.collate([sample]) results = self.infer_batch(batch) self._write_object_output(image, results[0]) return image