Skip to content

loaders

sleap_nn.inference.loaders

Standalone checkpoint loading for the inference pipeline.

Nothing user-facing here -- the public API remains :class:sleap_nn.inference.Predictor via :meth:Predictor.from_model_paths.

Classes:

Name Description
LoadedAssets

Everything the factory's _build_*_layer helpers need.

Functions:

Name Description
load_model_assets

Load checkpoints and build inference models.

LoadedAssets

Everything the factory's _build_*_layer helpers need.

Source code in sleap_nn/inference/loaders.py
@attrs.define(eq=False, repr=False)
class LoadedAssets:
    """Everything the factory's ``_build_*_layer`` helpers need."""

    inference_model: Any  # Union of all *InferenceModel types
    preprocess_config: "DictConfig"
    skeletons: list["sio.Skeleton"]

    bottomup_config: Optional["DictConfig"] = None
    backbone_type: Optional[str] = None
    max_stride: Optional[int] = None

    centroid_config: Optional["DictConfig"] = None
    confmap_config: Optional["DictConfig"] = None

    # Cap on instances per frame. Threaded through to the bottom-up grouping
    # stage (the legacy ``BottomUpInferenceModel`` has no such field, so the
    # value is carried on the assets and read by ``_build_bottomup_layer``).
    max_instances: Optional[int] = None

load_model_assets(model_paths, *, device='cpu', backbone_ckpt_path=None, head_ckpt_path=None, peak_threshold=0.2, integral_refinement='integral', integral_patch_size=5, max_instances=None, return_confmaps=False, preprocess_config=None, anchor_part=None, max_edge_length_ratio=0.25, dist_penalty_weight=1.0, n_points=10, min_instance_peaks=0, min_line_scores=0.25, fg_threshold=0.5, min_mask_area=0, center_nms_kernel=3, mask_cleanup=False, mask_cleanup_radius=0, distance_gate_alpha=None, merge_fragments=False, merge_method='greedy', merge_thresholds=(0.85, 0.6, 0.4), merge_w_valley=1.0, merge_w_offset=0.25, merge_dilate=1, full_res_masks=False, mask_output='mask', polygon_epsilon=0.01)

Load checkpoints and build inference models.

Each entry in model_paths may be a model directory, or a path to its best.ckpt checkpoint or training_config.{yaml,json} config file; every form is resolved to the model directory (#575).

The five bottom-up PAF grouping knobs (max_edge_length_ratio, dist_penalty_weight, n_points, min_instance_peaks, min_line_scores) are forwarded ONLY to the plain bottom-up builder (legacy applied them only to BottomUpPredictor); they are inert for other model types. Likewise fg_threshold / min_mask_area are forwarded ONLY to the bottom-up segmentation builder.

Returns:

Type Description
tuple[LoadedAssets, List[str]]

(loaded_assets, model_types)model_types is the list of detected model types (one per path in model_paths).

Source code in sleap_nn/inference/loaders.py
def load_model_assets(
    model_paths: List[str],
    *,
    device: str = "cpu",
    backbone_ckpt_path: Optional[str] = None,
    head_ckpt_path: Optional[str] = None,
    peak_threshold: Union[float, List[float]] = 0.2,
    integral_refinement: str = "integral",
    integral_patch_size: int = 5,
    max_instances: Optional[int] = None,
    return_confmaps: bool = False,
    preprocess_config: Optional["DictConfig"] = None,
    anchor_part: Optional[str] = None,
    max_edge_length_ratio: float = 0.25,
    dist_penalty_weight: float = 1.0,
    n_points: int = 10,
    min_instance_peaks: Union[int, float] = 0,
    min_line_scores: float = 0.25,
    fg_threshold: float = 0.5,
    min_mask_area: int = 0,
    center_nms_kernel: int = 3,
    mask_cleanup: bool = False,
    mask_cleanup_radius: int = 0,
    distance_gate_alpha: Optional[float] = None,
    merge_fragments: bool = False,
    merge_method: str = "greedy",
    merge_thresholds: tuple = (0.85, 0.6, 0.4),
    merge_w_valley: float = 1.0,
    merge_w_offset: float = 0.25,
    merge_dilate: int = 1,
    full_res_masks: bool = False,
    mask_output: str = "mask",
    polygon_epsilon: float = 0.01,
) -> tuple[LoadedAssets, List[str]]:
    """Load checkpoints and build inference models.

    Each entry in ``model_paths`` may be a model directory, or a path to its
    ``best.ckpt`` checkpoint or ``training_config.{yaml,json}`` config file; every
    form is resolved to the model directory (#575).

    The five bottom-up PAF grouping knobs (``max_edge_length_ratio``,
    ``dist_penalty_weight``, ``n_points``, ``min_instance_peaks``,
    ``min_line_scores``) are forwarded ONLY to the plain bottom-up builder
    (legacy applied them only to ``BottomUpPredictor``); they are inert for
    other model types. Likewise ``fg_threshold`` / ``min_mask_area`` are
    forwarded ONLY to the bottom-up segmentation builder.

    Returns:
        ``(loaded_assets, model_types)`` — *model_types* is the list of
        detected model types (one per path in *model_paths*).
    """
    if preprocess_config is None:
        preprocess_config = OmegaConf.create(
            {
                "ensure_rgb": None,
                "ensure_grayscale": None,
                "crop_size": None,
                "max_width": None,
                "max_height": None,
                "scale": None,
            }
        )

    # Accept a model directory, a best.ckpt path, or a training_config.{yaml,json}
    # path for each entry; resolve every form to its model directory before
    # detection/dispatch (the builders below index back into model_paths and join
    # `best.ckpt` onto it). #575.
    model_paths = [resolve_model_dir(mp) for mp in model_paths]

    model_types: List[str] = []
    configs: List[Any] = []
    for mp in model_paths:
        cfg, _ = _load_training_config(mp)
        configs.append(cfg)
        model_types.append(get_model_type_from_cfg(config=cfg))

    # Reject duplicate model types up front. The dispatch below picks the
    # FIRST path of a given type via `model_types.index(...)`, so passing two
    # paths of the same type (e.g. two centroid dirs) would otherwise
    # silently use only one and drop the other with no indication anything
    # was wrong. (Detecting an *unrelated* extra path -- one whose type isn't
    # consumed by whichever branch below ends up winning -- would need
    # dispatch-branch-aware validation; that's a separate, bigger follow-up,
    # not covered here.)
    _seen_type_paths: dict = {}
    for mp, mt in zip(model_paths, model_types):
        if mt in _seen_type_paths:
            raise ValueError(
                f"Duplicate model type {mt!r} in --model_paths: got both "
                f"{_seen_type_paths[mt]!r} and {mp!r}. Pass only one model "
                "directory per type."
            )
        _seen_type_paths[mt] = mp

    common_kwargs = dict(
        device=device,
        backbone_ckpt_path=backbone_ckpt_path,
        head_ckpt_path=head_ckpt_path,
        peak_threshold=peak_threshold,
        integral_refinement=integral_refinement,
        integral_patch_size=integral_patch_size,
        return_confmaps=return_confmaps,
        preprocess_config=preprocess_config,
    )

    # Dispatch on detected model type. The order (bottomup checked before the
    # topdown family) differs from legacy but is inert for parity: each model
    # path is exactly one type and bottom-up is a single-stage model never
    # combined with a centroid/centered-instance pair, so the branches are
    # mutually exclusive (a mixed bottomup+topdown path list is not a supported
    # workflow and falls through to the final ValueError). #584.
    if "single_instance" in model_types:
        path = model_paths[model_types.index("single_instance")]
        assets = _build_single_instance(path, **common_kwargs)

    elif "bottomup" in model_types:
        path = model_paths[model_types.index("bottomup")]
        assets = _build_bottomup(
            path,
            max_instances=max_instances,
            max_edge_length_ratio=max_edge_length_ratio,
            dist_penalty_weight=dist_penalty_weight,
            n_points=n_points,
            min_instance_peaks=min_instance_peaks,
            min_line_scores=min_line_scores,
            **common_kwargs,
        )

    elif "multi_class_bottomup" in model_types:
        path = model_paths[model_types.index("multi_class_bottomup")]
        assets = _build_bottomup_multiclass(
            path, max_instances=max_instances, **common_kwargs
        )

    elif "bottomup_segmentation" in model_types:
        path = model_paths[model_types.index("bottomup_segmentation")]
        assets = _build_bottomup_segmentation(
            path,
            fg_threshold=fg_threshold,
            min_mask_area=min_mask_area,
            max_instances=max_instances,
            center_nms_kernel=center_nms_kernel,
            mask_cleanup=mask_cleanup,
            mask_cleanup_radius=mask_cleanup_radius,
            distance_gate_alpha=distance_gate_alpha,
            merge_fragments=merge_fragments,
            merge_method=merge_method,
            merge_thresholds=merge_thresholds,
            merge_w_valley=merge_w_valley,
            merge_w_offset=merge_w_offset,
            merge_dilate=merge_dilate,
            full_res_masks=full_res_masks,
            mask_output=mask_output,
            polygon_epsilon=polygon_epsilon,
            **common_kwargs,
        )

    elif "semantic_segmentation" in model_types:
        # Whole-frame semantic (foreground/background) segmentation. A single-stage
        # whole-frame model (never combined with a centroid), so it gets its own
        # top-level branch alongside bottomup_segmentation.
        path = model_paths[model_types.index("semantic_segmentation")]
        assets = _build_semantic_segmentation(
            path,
            fg_threshold=fg_threshold,
            min_mask_area=min_mask_area,
            full_res_masks=full_res_masks,
            mask_output=mask_output,
            polygon_epsilon=polygon_epsilon,
            **common_kwargs,
        )

    elif "embedding" in model_types:
        # Appearance-embedding (re-ID). Checked BEFORE the topdown family block:
        # a centroid + embedding pair has "centroid" in model_types, so it would
        # otherwise enter that block and silently drop the embedding dir.
        #   - centroid + embedding -> compose centroid -> crop -> embed on raw
        #     video (TopDownEmbeddingLayer).
        #   - embedding alone -> single-stage, mask-driven case (EmbeddingLayer).
        emb_path = model_paths[model_types.index("embedding")]
        centroid_path = (
            model_paths[model_types.index("centroid")]
            if "centroid" in model_types
            else None
        )
        if centroid_path is not None:
            assets = _build_topdown_embedding(
                centroid_path,
                emb_path,
                max_instances=max_instances,
                anchor_part=anchor_part,
                **common_kwargs,
            )
        else:
            assets = _build_embedding(emb_path, **common_kwargs)

    elif "centered_instance_segmentation" in model_types:
        # Top-down (crop-centered) instance segmentation. MUST be checked BEFORE
        # the topdown family block below: a centroid + centered_instance_segmentation
        # pair has "centroid" in model_types, so it would otherwise enter that block
        # and — finding neither "centered_instance" nor "multi_class_topdown" — fall
        # to the centroid-only else, SILENTLY DROPPING the seg dir (verify/v2 #1).
        seg_path = model_paths[model_types.index("centered_instance_segmentation")]
        centroid_path = (
            model_paths[model_types.index("centroid")]
            if "centroid" in model_types
            else None
        )
        assets = _build_topdown_segmentation(
            centroid_path,
            seg_path,
            max_instances=max_instances,
            anchor_part=anchor_part,
            fg_threshold=fg_threshold,
            mask_output=mask_output,
            polygon_epsilon=polygon_epsilon,
            **common_kwargs,
        )

    elif (
        "centroid" in model_types
        or "centered_instance" in model_types
        or "multi_class_topdown" in model_types
    ):
        centroid_path = None
        confmap_path = None
        if "centroid" in model_types:
            centroid_path = model_paths[model_types.index("centroid")]
        if "centered_instance" in model_types:
            confmap_path = model_paths[model_types.index("centered_instance")]
            assets = _build_topdown(
                centroid_path,
                confmap_path,
                max_instances=max_instances,
                anchor_part=anchor_part,
                **common_kwargs,
            )
        elif "multi_class_topdown" in model_types:
            confmap_path = model_paths[model_types.index("multi_class_topdown")]
            assets = _build_topdown_multiclass(
                centroid_path,
                confmap_path,
                max_instances=max_instances,
                anchor_part=anchor_part,
                **common_kwargs,
            )
        else:
            # centroid-only: still goes through _build_topdown with confmap=None
            assets = _build_topdown(
                centroid_path,
                None,
                max_instances=max_instances,
                anchor_part=anchor_part,
                **common_kwargs,
            )
    else:
        raise ValueError(
            f"Could not create inference assets from model paths:\n{model_paths}\n"
            f"Detected types: {model_types}"
        )

    return assets, model_types