Skip to content

Combine

For a full guide including backend selection, usage examples, and per-category postprocessing, see the Postprocessing Backends page.

sahi.postprocess.combine

Postprocessing strategies for combining predictions from sliced inference.

Classes

PostprocessPredictions

PostprocessPredictions(
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
)

Bases: ABC

Abstract base class for postprocessing object prediction lists.

Subclasses implement a specific strategy (NMS, NMM, greedy NMM, etc.) to reduce overlapping detections produced by sliced inference.

Parameters:

Name Type Description Default
match_threshold
float

Minimum overlap value (IoU or IoS) to consider two predictions as matching.

0.5
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
class_agnostic
bool

If True, apply postprocessing across all categories. If False, apply per category independently.

True

Initialize the postprocessor with configuration parameters.

Parameters:

Name Type Description Default
match_threshold
float

Minimum overlap value (IoU or IoS) to consider two predictions as matching.

0.5
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
class_agnostic
bool

If True, apply postprocessing across all categories. If False, apply per category independently.

True
Source code in sahi/postprocess/combine.py
def __init__(
    self,
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
) -> None:
    """Initialize the postprocessor with configuration parameters.

    Args:
        match_threshold: Minimum overlap value (IoU or IoS) to consider
            two predictions as matching.
        match_metric: Overlap metric, "IOU" or "IOS".
        class_agnostic: If True, apply postprocessing across all
            categories. If False, apply per category independently.
    """
    self.match_threshold = match_threshold
    self.class_agnostic = class_agnostic
    self.match_metric = match_metric

NMSPostprocess

NMSPostprocess(
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
)

Bases: PostprocessPredictions

Postprocessor using Non-Maximum Suppression (NMS).

Keeps the highest-scored prediction among overlapping boxes and discards the rest. Does not merge bounding boxes or masks.

Source code in sahi/postprocess/combine.py
def __init__(
    self,
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
) -> None:
    """Initialize the postprocessor with configuration parameters.

    Args:
        match_threshold: Minimum overlap value (IoU or IoS) to consider
            two predictions as matching.
        match_metric: Overlap metric, "IOU" or "IOS".
        class_agnostic: If True, apply postprocessing across all
            categories. If False, apply per category independently.
    """
    self.match_threshold = match_threshold
    self.class_agnostic = class_agnostic
    self.match_metric = match_metric

NMMPostprocess

NMMPostprocess(
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
)

Bases: PostprocessPredictions

Postprocessor using Non-Maximum Merging (NMM) with transitive merging.

Instead of discarding overlapping detections, merges their bounding boxes, masks, and scores. Uses non-greedy transitive merging: if A overlaps B and B overlaps C, all three are merged even if A does not directly overlap C.

Source code in sahi/postprocess/combine.py
def __init__(
    self,
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
) -> None:
    """Initialize the postprocessor with configuration parameters.

    Args:
        match_threshold: Minimum overlap value (IoU or IoS) to consider
            two predictions as matching.
        match_metric: Overlap metric, "IOU" or "IOS".
        class_agnostic: If True, apply postprocessing across all
            categories. If False, apply per category independently.
    """
    self.match_threshold = match_threshold
    self.class_agnostic = class_agnostic
    self.match_metric = match_metric

GreedyNMMPostprocess

GreedyNMMPostprocess(
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
)

Bases: NMMPostprocess

Postprocessor using Greedy Non-Maximum Merging (NMM).

Similar to NMM but uses a greedy strategy: each kept prediction only merges boxes that directly overlap with it (no transitive merging). This is faster than full NMM and produces tighter merged boxes.

Source code in sahi/postprocess/combine.py
def __init__(
    self,
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
) -> None:
    """Initialize the postprocessor with configuration parameters.

    Args:
        match_threshold: Minimum overlap value (IoU or IoS) to consider
            two predictions as matching.
        match_metric: Overlap metric, "IOU" or "IOS".
        class_agnostic: If True, apply postprocessing across all
            categories. If False, apply per category independently.
    """
    self.match_threshold = match_threshold
    self.class_agnostic = class_agnostic
    self.match_metric = match_metric

LSNMSPostprocess

LSNMSPostprocess(
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
)

Bases: PostprocessPredictions

Postprocessor using Locality-Sensitive NMS from the lsnms package.

Uses a spatial index for fast neighbor lookup, making it efficient for large numbers of predictions. Only supports IoU metric (not IoS). Requires the lsnms package (pip install lsnms>0.3.1).

Note

This postprocessor is experimental and not recommended for production use.

Source code in sahi/postprocess/combine.py
def __init__(
    self,
    match_threshold: float = 0.5,
    match_metric: str = "IOU",
    class_agnostic: bool = True,
) -> None:
    """Initialize the postprocessor with configuration parameters.

    Args:
        match_threshold: Minimum overlap value (IoU or IoS) to consider
            two predictions as matching.
        match_metric: Overlap metric, "IOU" or "IOS".
        class_agnostic: If True, apply postprocessing across all
            categories. If False, apply per category independently.
    """
    self.match_threshold = match_threshold
    self.class_agnostic = class_agnostic
    self.match_metric = match_metric

Functions:

nms

nms(
    predictions: ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> list[int]

Non-maximum suppression for axis-aligned bounding boxes.

Dispatches to the resolved backend (numpy, numba, or torchvision).

Parameters:

Name Type Description Default
predictions
ndarray

Array of shape (N, 6) with columns [x1, y1, x2, y2, score, category_id].

required
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
match_threshold
float

Minimum overlap to suppress a lower-scored box.

0.5

Returns:

Type Description
list[int]

List of indices of the kept predictions, sorted by score descending.

Source code in sahi/postprocess/combine.py
def nms(
    predictions: np.ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> list[int]:
    """Non-maximum suppression for axis-aligned bounding boxes.

    Dispatches to the resolved backend (numpy, numba, or torchvision).

    Args:
        predictions: Array of shape (N, 6) with columns
            [x1, y1, x2, y2, score, category_id].
        match_metric: Overlap metric, "IOU" or "IOS".
        match_threshold: Minimum overlap to suppress a lower-scored box.

    Returns:
        List of indices of the kept predictions, sorted by score descending.
    """
    if len(predictions) == 0:
        return []
    return _dispatch("nms")(predictions, match_metric, match_threshold)

batched_nms

batched_nms(
    predictions: ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> list[int]

Apply non-maximum suppression independently per category.

Parameters:

Name Type Description Default
predictions
ndarray

Array of shape (N, 6) with columns [x1, y1, x2, y2, score, category_id].

required
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
match_threshold
float

Minimum overlap to suppress a lower-scored box.

0.5

Returns:

Type Description
list[int]

List of indices of the kept predictions, sorted by score descending.

Source code in sahi/postprocess/combine.py
def batched_nms(
    predictions: np.ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> list[int]:
    """Apply non-maximum suppression independently per category.

    Args:
        predictions: Array of shape (N, 6) with columns
            [x1, y1, x2, y2, score, category_id].
        match_metric: Overlap metric, "IOU" or "IOS".
        match_threshold: Minimum overlap to suppress a lower-scored box.

    Returns:
        List of indices of the kept predictions, sorted by score descending.
    """
    return _batched_apply(predictions, nms, match_metric, match_threshold)  # type: ignore[return-value]

greedy_nmm

greedy_nmm(
    predictions: ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]

Greedy non-maximum merging for axis-aligned bounding boxes.

Instead of discarding overlapping boxes, merges them into the highest-scored box. Dispatches to the resolved backend.

Parameters:

Name Type Description Default
predictions
ndarray

Array of shape (N, 6) with columns [x1, y1, x2, y2, score, category_id].

required
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
match_threshold
float

Minimum overlap to merge a lower-scored box.

0.5

Returns:

Type Description
dict[int, list[int]]

Dict mapping each kept index to a list of indices merged into it.

Source code in sahi/postprocess/combine.py
def greedy_nmm(
    predictions: np.ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]:
    """Greedy non-maximum merging for axis-aligned bounding boxes.

    Instead of discarding overlapping boxes, merges them into the
    highest-scored box. Dispatches to the resolved backend.

    Args:
        predictions: Array of shape (N, 6) with columns
            [x1, y1, x2, y2, score, category_id].
        match_metric: Overlap metric, "IOU" or "IOS".
        match_threshold: Minimum overlap to merge a lower-scored box.

    Returns:
        Dict mapping each kept index to a list of indices merged into it.
    """
    return _dispatch("greedy_nmm")(predictions, match_metric, match_threshold)

batched_greedy_nmm

batched_greedy_nmm(
    predictions: ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]

Apply greedy non-maximum merging independently per category.

Parameters:

Name Type Description Default
predictions
ndarray

Array of shape (N, 6) with columns [x1, y1, x2, y2, score, category_id].

required
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
match_threshold
float

Minimum overlap to merge a lower-scored box.

0.5

Returns:

Type Description
dict[int, list[int]]

Dict mapping each kept index to a list of indices merged into it.

Source code in sahi/postprocess/combine.py
def batched_greedy_nmm(
    predictions: np.ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]:
    """Apply greedy non-maximum merging independently per category.

    Args:
        predictions: Array of shape (N, 6) with columns
            [x1, y1, x2, y2, score, category_id].
        match_metric: Overlap metric, "IOU" or "IOS".
        match_threshold: Minimum overlap to merge a lower-scored box.

    Returns:
        Dict mapping each kept index to a list of indices merged into it.
    """
    return _batched_apply(predictions, greedy_nmm, match_metric, match_threshold)  # type: ignore[return-value]

nmm

nmm(
    predictions: ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]

Non-maximum merging (non-greedy, transitive) for axis-aligned bounding boxes.

Unlike greedy NMM, this variant allows transitive merging: if box A merges with B and B merges with C, all three are merged together. Dispatches to the resolved backend.

Parameters:

Name Type Description Default
predictions
ndarray

Array of shape (N, 6) with columns [x1, y1, x2, y2, score, category_id].

required
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
match_threshold
float

Minimum overlap to merge a lower-scored box.

0.5

Returns:

Type Description
dict[int, list[int]]

Dict mapping each kept index to a list of indices merged into it.

Source code in sahi/postprocess/combine.py
def nmm(
    predictions: np.ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]:
    """Non-maximum merging (non-greedy, transitive) for axis-aligned bounding boxes.

    Unlike greedy NMM, this variant allows transitive merging: if box A
    merges with B and B merges with C, all three are merged together.
    Dispatches to the resolved backend.

    Args:
        predictions: Array of shape (N, 6) with columns
            [x1, y1, x2, y2, score, category_id].
        match_metric: Overlap metric, "IOU" or "IOS".
        match_threshold: Minimum overlap to merge a lower-scored box.

    Returns:
        Dict mapping each kept index to a list of indices merged into it.
    """
    return _dispatch("nmm")(predictions, match_metric, match_threshold)

batched_nmm

batched_nmm(
    predictions: ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]

Apply non-maximum merging (non-greedy, transitive) independently per category.

Parameters:

Name Type Description Default
predictions
ndarray

Array of shape (N, 6) with columns [x1, y1, x2, y2, score, category_id].

required
match_metric
str

Overlap metric, "IOU" or "IOS".

'IOU'
match_threshold
float

Minimum overlap to merge a lower-scored box.

0.5

Returns:

Type Description
dict[int, list[int]]

Dict mapping each kept index to a list of indices merged into it.

Source code in sahi/postprocess/combine.py
def batched_nmm(
    predictions: np.ndarray,
    match_metric: str = "IOU",
    match_threshold: float = 0.5,
) -> dict[int, list[int]]:
    """Apply non-maximum merging (non-greedy, transitive) independently per category.

    Args:
        predictions: Array of shape (N, 6) with columns
            [x1, y1, x2, y2, score, category_id].
        match_metric: Overlap metric, "IOU" or "IOS".
        match_threshold: Minimum overlap to merge a lower-scored box.

    Returns:
        Dict mapping each kept index to a list of indices merged into it.
    """
    return _batched_apply(predictions, nmm, match_metric, match_threshold)  # type: ignore[return-value]