Skip to content

filters

sleap_nn.inference.ops.filters

Inference-level postprocessing filters for pose predictions.

This module provides filters that run after model inference but before tracking. These filters are independent of tracking configuration and can be used standalone.

Functions:

Name Description
filter_by_node_confidence

Filter instances with low confidence scores.

filter_by_node_count

Filter instances with insufficient visible keypoints.

filter_overlapping_instances

Filter overlapping instances using greedy non-maximum suppression.

filter_by_node_confidence(labels, min_mean_node_score=0.0, min_instance_score=0.0)

Filter instances with low confidence scores.

Removes predicted instances based on their per-node confidence scores and/or overall instance score. This is useful for removing uncertain predictions that may have passed the peak threshold but are still low quality.

This filter runs independently of tracking and can be used to clean up model outputs before saving or further processing.

Parameters:

Name Type Description Default
labels Labels

Labels object with predicted instances to filter.

required
min_mean_node_score float

Minimum mean confidence score across visible nodes. The mean is computed only over non-NaN keypoints. Default: 0.0 (no filtering by mean node score).

0.0
min_instance_score float

Minimum overall instance confidence score. Default: 0.0 (no filtering by instance score).

0.0

Returns:

Type Description
Labels

The input Labels object with low-confidence instances removed. Modification is done in place, but the object is also returned for convenience.

Example

Require mean node confidence >= 0.5

labels = filter_by_node_confidence(labels, min_mean_node_score=0.5)

Require instance score >= 0.3

labels = filter_by_node_confidence(labels, min_instance_score=0.3)

Combine both criteria

labels = filter_by_node_confidence( ... labels, min_mean_node_score=0.4, min_instance_score=0.2 ... )

Note
  • Only affects predicted instances (preserves ground truth instances)
  • An instance must pass ALL specified criteria to be kept
  • If point_scores is not available, mean node score check is skipped
  • If instance score is not available, instance score check is skipped
Source code in sleap_nn/inference/ops/filters.py
def filter_by_node_confidence(
    labels: sio.Labels,
    min_mean_node_score: float = 0.0,
    min_instance_score: float = 0.0,
) -> sio.Labels:
    """Filter instances with low confidence scores.

    Removes predicted instances based on their per-node confidence scores
    and/or overall instance score. This is useful for removing uncertain
    predictions that may have passed the peak threshold but are still
    low quality.

    This filter runs independently of tracking and can be used to clean up
    model outputs before saving or further processing.

    Args:
        labels: Labels object with predicted instances to filter.
        min_mean_node_score: Minimum mean confidence score across visible nodes.
            The mean is computed only over non-NaN keypoints.
            Default: 0.0 (no filtering by mean node score).
        min_instance_score: Minimum overall instance confidence score.
            Default: 0.0 (no filtering by instance score).

    Returns:
        The input Labels object with low-confidence instances removed.
        Modification is done in place, but the object is also returned
        for convenience.

    Example:
        >>> # Require mean node confidence >= 0.5
        >>> labels = filter_by_node_confidence(labels, min_mean_node_score=0.5)
        >>> # Require instance score >= 0.3
        >>> labels = filter_by_node_confidence(labels, min_instance_score=0.3)
        >>> # Combine both criteria
        >>> labels = filter_by_node_confidence(
        ...     labels, min_mean_node_score=0.4, min_instance_score=0.2
        ... )

    Note:
        - Only affects predicted instances (preserves ground truth instances)
        - An instance must pass ALL specified criteria to be kept
        - If point_scores is not available, mean node score check is skipped
        - If instance score is not available, instance score check is skipped
    """
    # Early exit if no filtering requested
    if min_mean_node_score <= 0.0 and min_instance_score <= 0.0:
        return labels

    for lf in labels.labeled_frames:
        if len(lf.instances) == 0:
            continue

        kept_instances = []
        for inst in lf.instances:
            # Only filter predicted instances
            if not isinstance(inst, sio.PredictedInstance):
                kept_instances.append(inst)
                continue

            # Check instance score criterion
            if min_instance_score > 0.0:
                inst_score = _instance_score(inst)
                if inst_score < min_instance_score:
                    continue

            # Check mean node score criterion
            if min_mean_node_score > 0.0:
                mean_score = _mean_node_score(inst)
                if mean_score is not None and mean_score < min_mean_node_score:
                    continue

            # Instance passed all criteria
            kept_instances.append(inst)

        lf.instances = kept_instances

    return labels

filter_by_node_count(labels, min_visible_nodes=0, min_visible_node_fraction=0.0)

Filter instances with insufficient visible keypoints.

Removes predicted instances that have too few detected/visible keypoints. This is useful for cleaning up spurious detections that only have 1-2 nodes or for requiring a minimum skeleton completeness.

This filter runs independently of tracking and can be used to clean up model outputs before saving or further processing.

Parameters:

Name Type Description Default
labels Labels

Labels object with predicted instances to filter.

required
min_visible_nodes int

Minimum number of visible (non-NaN) keypoints required. Instances with fewer visible nodes are removed. Default: 0 (no filtering by absolute count).

0
min_visible_node_fraction float

Minimum fraction of skeleton nodes that must be visible. Value should be in [0, 1]. For example, 0.5 requires at least half of the skeleton's nodes to be detected. Default: 0.0 (no filtering by fraction).

0.0

Returns:

Type Description
Labels

The input Labels object with low-node-count instances removed. Modification is done in place, but the object is also returned for convenience.

Example

Require at least 3 visible nodes

labels = filter_by_node_count(labels, min_visible_nodes=3)

Require at least 50% of skeleton nodes

labels = filter_by_node_count(labels, min_visible_node_fraction=0.5)

Combine both criteria (must pass both)

labels = filter_by_node_count( ... labels, min_visible_nodes=2, min_visible_node_fraction=0.3 ... )

Note
  • Only affects predicted instances (preserves ground truth instances)
  • An instance must pass ALL specified criteria to be kept
  • A keypoint is "visible" if its coordinates are not NaN
Source code in sleap_nn/inference/ops/filters.py
def filter_by_node_count(
    labels: sio.Labels,
    min_visible_nodes: int = 0,
    min_visible_node_fraction: float = 0.0,
) -> sio.Labels:
    """Filter instances with insufficient visible keypoints.

    Removes predicted instances that have too few detected/visible keypoints.
    This is useful for cleaning up spurious detections that only have 1-2 nodes
    or for requiring a minimum skeleton completeness.

    This filter runs independently of tracking and can be used to clean up
    model outputs before saving or further processing.

    Args:
        labels: Labels object with predicted instances to filter.
        min_visible_nodes: Minimum number of visible (non-NaN) keypoints required.
            Instances with fewer visible nodes are removed.
            Default: 0 (no filtering by absolute count).
        min_visible_node_fraction: Minimum fraction of skeleton nodes that must
            be visible. Value should be in [0, 1]. For example, 0.5 requires at
            least half of the skeleton's nodes to be detected.
            Default: 0.0 (no filtering by fraction).

    Returns:
        The input Labels object with low-node-count instances removed.
        Modification is done in place, but the object is also returned
        for convenience.

    Example:
        >>> # Require at least 3 visible nodes
        >>> labels = filter_by_node_count(labels, min_visible_nodes=3)
        >>> # Require at least 50% of skeleton nodes
        >>> labels = filter_by_node_count(labels, min_visible_node_fraction=0.5)
        >>> # Combine both criteria (must pass both)
        >>> labels = filter_by_node_count(
        ...     labels, min_visible_nodes=2, min_visible_node_fraction=0.3
        ... )

    Note:
        - Only affects predicted instances (preserves ground truth instances)
        - An instance must pass ALL specified criteria to be kept
        - A keypoint is "visible" if its coordinates are not NaN
    """
    # Early exit if no filtering requested
    if min_visible_nodes <= 0 and min_visible_node_fraction <= 0.0:
        return labels

    for lf in labels.labeled_frames:
        if len(lf.instances) == 0:
            continue

        kept_instances = []
        for inst in lf.instances:
            # Only filter predicted instances
            if not isinstance(inst, sio.PredictedInstance):
                kept_instances.append(inst)
                continue

            # Count visible nodes
            n_visible = _count_visible_nodes(inst)
            n_total = len(inst.skeleton.nodes)

            # Check absolute count criterion
            if min_visible_nodes > 0 and n_visible < min_visible_nodes:
                continue

            # Check fraction criterion
            if min_visible_node_fraction > 0.0:
                fraction = n_visible / n_total if n_total > 0 else 0.0
                if fraction < min_visible_node_fraction:
                    continue

            # Instance passed all criteria
            kept_instances.append(inst)

        lf.instances = kept_instances

    return labels

filter_overlapping_instances(labels, threshold=0.8, method='iou')

Filter overlapping instances using greedy non-maximum suppression.

Removes duplicate/overlapping instances by applying greedy NMS based on either bounding box IOU or Object Keypoint Similarity (OKS). When two instances overlap above the threshold, the lower-scoring one is removed.

This filter runs independently of tracking and can be used to clean up model outputs before saving or further processing.

Parameters:

Name Type Description Default
labels Labels

Labels object with predicted instances to filter.

required
threshold float

Similarity threshold for considering instances as overlapping. Instances with similarity > threshold are candidates for removal. Lower values are more aggressive (remove more). Typical values: 0.3 (aggressive) to 0.8 (permissive).

0.8
method Literal['iou', 'oks']

Similarity metric to use for comparing instances. "iou": Bounding box intersection-over-union. "oks": Object Keypoint Similarity (pose-based).

'iou'

Returns:

Type Description
Labels

The input Labels object with overlapping instances removed. Modification is done in place, but the object is also returned for convenience.

Example

Filter instances with >80% bounding box overlap

labels = filter_overlapping_instances(labels, threshold=0.8, method="iou")

Filter using OKS similarity

labels = filter_overlapping_instances(labels, threshold=0.5, method="oks")

Note
  • Only affects frames with 2+ predicted instances
  • Uses instance.score for ranking; higher scores are preferred
  • For IOU: bounding boxes computed from non-NaN keypoints
  • For OKS: uses standard COCO OKS formula with bbox-derived scale
Source code in sleap_nn/inference/ops/filters.py
def filter_overlapping_instances(
    labels: sio.Labels,
    threshold: float = 0.8,
    method: Literal["iou", "oks"] = "iou",
) -> sio.Labels:
    """Filter overlapping instances using greedy non-maximum suppression.

    Removes duplicate/overlapping instances by applying greedy NMS based on
    either bounding box IOU or Object Keypoint Similarity (OKS). When two
    instances overlap above the threshold, the lower-scoring one is removed.

    This filter runs independently of tracking and can be used to clean up
    model outputs before saving or further processing.

    Args:
        labels: Labels object with predicted instances to filter.
        threshold: Similarity threshold for considering instances as overlapping.
            Instances with similarity > threshold are candidates for removal.
            Lower values are more aggressive (remove more).
            Typical values: 0.3 (aggressive) to 0.8 (permissive).
        method: Similarity metric to use for comparing instances.
            "iou": Bounding box intersection-over-union.
            "oks": Object Keypoint Similarity (pose-based).

    Returns:
        The input Labels object with overlapping instances removed.
        Modification is done in place, but the object is also returned
        for convenience.

    Example:
        >>> # Filter instances with >80% bounding box overlap
        >>> labels = filter_overlapping_instances(labels, threshold=0.8, method="iou")
        >>> # Filter using OKS similarity
        >>> labels = filter_overlapping_instances(labels, threshold=0.5, method="oks")

    Note:
        - Only affects frames with 2+ predicted instances
        - Uses instance.score for ranking; higher scores are preferred
        - For IOU: bounding boxes computed from non-NaN keypoints
        - For OKS: uses standard COCO OKS formula with bbox-derived scale
    """
    for lf in labels.labeled_frames:
        if len(lf.instances) <= 1:
            continue

        # Separate predicted instances (have scores) from other instances
        predicted = []
        other = []
        for inst in lf.instances:
            if isinstance(inst, sio.PredictedInstance):
                predicted.append(inst)
            else:
                other.append(inst)

        # Only filter predicted instances
        if len(predicted) <= 1:
            continue

        # Get scores
        scores = np.array([_instance_score(inst) for inst in predicted])

        # Apply greedy NMS with selected method
        if method == "iou":
            bboxes = np.array([_instance_bbox(inst) for inst in predicted])
            keep_indices = _nms_greedy_iou(bboxes, scores, threshold)
        elif method == "oks":
            points = [inst.numpy() for inst in predicted]
            keep_indices = _nms_greedy_oks(points, scores, threshold)
        else:
            raise ValueError(f"Unknown method: {method}. Use 'iou' or 'oks'.")

        # Reconstruct instance list: kept predicted + other instances
        kept_predicted = [predicted[i] for i in keep_indices]
        lf.instances = kept_predicted + other

    return labels