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

GreedyNMMPostprocess

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
class GreedyNMMPostprocess(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.
    """

    _agnostic_func = staticmethod(greedy_nmm)
    _batched_func = staticmethod(batched_greedy_nmm)

LSNMSPostprocess

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
class LSNMSPostprocess(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.
    """

    def __call__(self, object_predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
        """Apply Locality-Sensitive NMS to suppress overlapping predictions.

        Args:
            object_predictions: List of ObjectPrediction instances to suppress.

        Returns:
            List of suppressed ObjectPrediction instances.

        Raises:
            ModuleNotFoundError: If the lsnms package is not installed.
            NotImplementedError: If match_metric is not "IOU".
        """
        try:
            from lsnms import nms
        except ModuleNotFoundError:
            raise ModuleNotFoundError(
                'Please run "pip install lsnms>0.3.1" to install lsnms first for lsnms utilities.'
            )

        if self.match_metric == "IOS":
            raise NotImplementedError(f"match_metric={self.match_metric} is not supported for LSNMSPostprocess")

        logger.warning("LSNMSPostprocess is experimental and not recommended to use.")

        object_prediction_list = ObjectPredictionList(object_predictions)
        preds_np = object_prediction_list.tonumpy()

        boxes = preds_np[:, :4]
        scores = preds_np[:, 4]
        class_ids = preds_np[:, 5].astype("uint8")

        keep = nms(
            boxes, scores, iou_threshold=self.match_threshold, class_ids=None if self.class_agnostic else class_ids
        )

        selected = object_prediction_list[keep].tolist()
        if not isinstance(selected, list):
            selected = [selected]
        return selected
Functions
__call__(object_predictions)

Apply Locality-Sensitive NMS to suppress overlapping predictions.

Parameters:

Name Type Description Default
object_predictions list[ObjectPrediction]

List of ObjectPrediction instances to suppress.

required

Returns:

Type Description
list[ObjectPrediction]

List of suppressed ObjectPrediction instances.

Raises:

Type Description
ModuleNotFoundError

If the lsnms package is not installed.

NotImplementedError

If match_metric is not "IOU".

Source code in sahi/postprocess/combine.py
def __call__(self, object_predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
    """Apply Locality-Sensitive NMS to suppress overlapping predictions.

    Args:
        object_predictions: List of ObjectPrediction instances to suppress.

    Returns:
        List of suppressed ObjectPrediction instances.

    Raises:
        ModuleNotFoundError: If the lsnms package is not installed.
        NotImplementedError: If match_metric is not "IOU".
    """
    try:
        from lsnms import nms
    except ModuleNotFoundError:
        raise ModuleNotFoundError(
            'Please run "pip install lsnms>0.3.1" to install lsnms first for lsnms utilities.'
        )

    if self.match_metric == "IOS":
        raise NotImplementedError(f"match_metric={self.match_metric} is not supported for LSNMSPostprocess")

    logger.warning("LSNMSPostprocess is experimental and not recommended to use.")

    object_prediction_list = ObjectPredictionList(object_predictions)
    preds_np = object_prediction_list.tonumpy()

    boxes = preds_np[:, :4]
    scores = preds_np[:, 4]
    class_ids = preds_np[:, 5].astype("uint8")

    keep = nms(
        boxes, scores, iou_threshold=self.match_threshold, class_ids=None if self.class_agnostic else class_ids
    )

    selected = object_prediction_list[keep].tolist()
    if not isinstance(selected, list):
        selected = [selected]
    return selected

NMMPostprocess

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
class NMMPostprocess(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.
    """

    _agnostic_func = staticmethod(nmm)
    _batched_func = staticmethod(batched_nmm)

    def __call__(self, object_predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
        """Apply Non-Maximum Merging to merge overlapping predictions.

        Args:
            object_predictions: List of ObjectPrediction instances to merge.

        Returns:
            List of merged ObjectPrediction instances.
        """
        object_prediction_list = ObjectPredictionList(object_predictions)
        preds_np = object_prediction_list.tonumpy()
        func = self._agnostic_func if self.class_agnostic else self._batched_func
        keep_to_merge = func(preds_np, match_threshold=self.match_threshold, match_metric=self.match_metric)
        return _apply_merge(object_prediction_list, keep_to_merge, self.match_metric, self.match_threshold)
Functions
__call__(object_predictions)

Apply Non-Maximum Merging to merge overlapping predictions.

Parameters:

Name Type Description Default
object_predictions list[ObjectPrediction]

List of ObjectPrediction instances to merge.

required

Returns:

Type Description
list[ObjectPrediction]

List of merged ObjectPrediction instances.

Source code in sahi/postprocess/combine.py
def __call__(self, object_predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
    """Apply Non-Maximum Merging to merge overlapping predictions.

    Args:
        object_predictions: List of ObjectPrediction instances to merge.

    Returns:
        List of merged ObjectPrediction instances.
    """
    object_prediction_list = ObjectPredictionList(object_predictions)
    preds_np = object_prediction_list.tonumpy()
    func = self._agnostic_func if self.class_agnostic else self._batched_func
    keep_to_merge = func(preds_np, match_threshold=self.match_threshold, match_metric=self.match_metric)
    return _apply_merge(object_prediction_list, keep_to_merge, self.match_metric, self.match_threshold)

NMSPostprocess

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
class NMSPostprocess(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.
    """

    def __call__(self, object_predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
        """Apply Non-Maximum Suppression to suppress overlapping predictions.

        Args:
            object_predictions: List of ObjectPrediction instances to suppress.

        Returns:
            List of suppressed ObjectPrediction instances.
        """
        object_prediction_list = ObjectPredictionList(object_predictions)
        preds_np = object_prediction_list.tonumpy()
        func = nms if self.class_agnostic else batched_nms
        keep = func(preds_np, match_threshold=self.match_threshold, match_metric=self.match_metric)

        selected = object_prediction_list[keep].tolist()
        if not isinstance(selected, list):
            selected = [selected]
        return selected
Functions
__call__(object_predictions)

Apply Non-Maximum Suppression to suppress overlapping predictions.

Parameters:

Name Type Description Default
object_predictions list[ObjectPrediction]

List of ObjectPrediction instances to suppress.

required

Returns:

Type Description
list[ObjectPrediction]

List of suppressed ObjectPrediction instances.

Source code in sahi/postprocess/combine.py
def __call__(self, object_predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
    """Apply Non-Maximum Suppression to suppress overlapping predictions.

    Args:
        object_predictions: List of ObjectPrediction instances to suppress.

    Returns:
        List of suppressed ObjectPrediction instances.
    """
    object_prediction_list = ObjectPredictionList(object_predictions)
    preds_np = object_prediction_list.tonumpy()
    func = nms if self.class_agnostic else batched_nms
    keep = func(preds_np, match_threshold=self.match_threshold, match_metric=self.match_metric)

    selected = object_prediction_list[keep].tolist()
    if not isinstance(selected, list):
        selected = [selected]
    return selected

PostprocessPredictions

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
Source code in sahi/postprocess/combine.py
class PostprocessPredictions(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.

    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.
    """

    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

    @abstractmethod
    def __call__(self, predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
        """Apply postprocessing to the list of predictions.

        Args:
            predictions: List of ObjectPrediction instances to postprocess.

        Returns:
            List of postprocessed ObjectPrediction instances.
        """
        pass
Functions
__call__(predictions) abstractmethod

Apply postprocessing to the list of predictions.

Parameters:

Name Type Description Default
predictions list[ObjectPrediction]

List of ObjectPrediction instances to postprocess.

required

Returns:

Type Description
list[ObjectPrediction]

List of postprocessed ObjectPrediction instances.

Source code in sahi/postprocess/combine.py
@abstractmethod
def __call__(self, predictions: list[ObjectPrediction]) -> list[ObjectPrediction]:
    """Apply postprocessing to the list of predictions.

    Args:
        predictions: List of ObjectPrediction instances to postprocess.

    Returns:
        List of postprocessed ObjectPrediction instances.
    """
    pass
__init__(match_threshold=0.5, match_metric='IOU', class_agnostic=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

Functions

batched_greedy_nmm(predictions, match_metric='IOU', match_threshold=0.5)

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]

batched_nmm(predictions, match_metric='IOU', match_threshold=0.5)

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]

batched_nms(predictions, match_metric='IOU', match_threshold=0.5)

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(predictions, match_metric='IOU', match_threshold=0.5)

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)

nmm(predictions, match_metric='IOU', match_threshold=0.5)

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)

nms(predictions, match_metric='IOU', match_threshold=0.5)

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)