Skip to content

feat: add box_weighted_box_fusion (Weighted Box Fusion) - #2455

Open
rs-03 wants to merge 2 commits into
roboflow:developfrom
rs-03:feat/weighted-box-fusion
Open

rs-03 wants to merge 2 commits into
roboflow:developfrom
rs-03:feat/weighted-box-fusion

Conversation

@rs-03

@rs-03 rs-03 commented Jul 25, 2026

Copy link
Copy Markdown
Before submitting
  • Self-reviewed the code
  • Updated documentation, follow Google-style
  • Added docs entry for autogeneration (if new functions/classes)
  • Added/updated tests
  • All tests pass locally

Description

Adds sv.box_weighted_box_fusion, an implementation of Weighted Box Fusion (WBF; Solovyev et al., 2019) as a confidence-weighted alternative to sv.box_non_max_suppression. Where NMS discards overlapping boxes and box_non_max_merge only returns the overlapping index groups, WBF replaces each group of overlapping same-class boxes with a single fused box whose coordinates are the confidence-weighted average of the group and whose score is the group's mean confidence. Keeping information from every box in a cluster, rather than dropping all but one, typically yields better localized boxes when combining predictions from several models or test-time augmentations.

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)

Motivation and Context

WBF is a widely used technique for ensembling object detectors, and adding it has been requested for a while.

Closes #268

The implementation reuses the existing per-category greedy grouping (box_non_max_merge) and box_iou_batch, so it stays consistent with the other functions in iou_and_nms.py and accepts the same (x1, y1, x2, y2, score) or (x1, y1, x2, y2, score, class) prediction format. Omitting the class column fuses across classes; including it fuses per class.

One design decision worth your input: the paper rescales the fused confidence by the number of contributing models. supervision's API takes a single predictions array with no per-model attribution, so I set the fused score to the group's mean confidence, which is the paper's formula with T equal to the group size. If you would prefer different behavior (for example an optional weights or model-count parameter, or summing then capping the confidence), I am glad to adjust.

Changes Made

  • New sv.box_weighted_box_fusion in detection/utils/iou_and_nms.py, with a private _fuse_box_group helper for the per-group confidence-weighted fusion (uniform average fallback when every score in a group is zero). Output is ordered by descending fused confidence and keeps the input column count.
  • Exported the function from the public API in supervision/__init__.py.
  • Added the autogeneration docs entry in docs/detection/utils/iou_and_nms.md.
  • Added a changelog entry under Unreleased.
  • Added tests in tests/detection/utils/test_iou_and_nms.py: 7 parametrized cases (empty input, single box, two overlapping same-class boxes, non-overlapping boxes kept and score-ordered, same box different class not fused, no-class-column, out-of-range threshold) plus two edge-case tests (all-zero scores use a uniform average, and the fused box lands closer to the higher-confidence member).

Testing

All 219 tests in tests/detection/utils/test_iou_and_nms.py pass, including the 9 new ones; ruff and mypy are clean on the new code, and the docstring doctest passes. Numeric check: for [0, 0, 10, 10] at score 0.9 and [1, 1, 11, 11] at score 0.8 (IoU 0.68), the fused box is [0.471, 0.471, 10.471, 10.471] at score 0.85. Since this is a deterministic pure-NumPy function, the unit tests serve as the reproduction rather than a Colab.

@rs-03
rs-03 requested a review from SkalskiP as a code owner July 25, 2026 18:59
@CLAassistant

CLAassistant commented Jul 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@amerob amerob left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see WBF finally land — #268 has been open a long time. Reusing box_non_max_merge and box_iou_batch keeps this consistent with the rest of iou_and_nms.py, and the all-zero-scores fallback in _fuse_box_group is a good touch.

Taking your open question head-on, plus two things I think need attention before this ships under the Solovyev name.

The missing rescale (your design question)

You're right that the paper's rescale needs a model count, and that the API doesn't have one. I'd add it as optional rather than drop it:

def box_weighted_box_fusion(
    predictions: npt.NDArray[np.floating],
    iou_threshold: float = 0.5,
    overlap_metric: OverlapMetric = OverlapMetric.IOU,
    num_models: int | None = None,
) -> npt.NDArray[np.floating]:

and when it's set, apply the paper's step to each cluster:

C = C * min(T, N) / N     # T = boxes in cluster, N = number of models

That keeps the single-array API intact for everyone else and makes the paper-faithful path reachable. It matters more than it looks: the rescale is the entire mechanism by which WBF penalizes a detection that only one model out of five produced. Without it, a single-model false positive sails through a one-box cluster with its score completely untouched — which is the main thing WBF is supposed to buy you over NMS.

Mean confidence quietly demotes strong detections

Separate from the rescale, and worth calling out explicitly because it will surprise people swapping box_non_max_suppressionbox_weighted_box_fusion:

fused_score = float(scores.mean())

Given a confident true positive at 0.95 and a spurious box at 0.15 that happens to clear the IoU threshold, the fused score is 0.55. NMS would have kept 0.95. So on a single model's output — which is how a lot of people will first try this — WBF can systematically lower scores on good detections and push them under a downstream confidence filter.

The reference implementation handles this with conf_type ('avg', 'max', 'box_and_model_avg', 'absent_model_aware_avg'). I don't think you need all four, but either a conf_type with 'avg'/'max' or a clear docstring warning that scores are averaged and can drop below the best box in the cluster would save people a confusing afternoon.

The clustering isn't WBF's clustering

This is the one I'd most want resolved, because the docstring says "Based on Solovyev et al." and people will benchmark it against ensemble_boxes.weighted_boxes_fusion.

_group_overlapping_boxes takes the highest-scoring box as a seed, matches every remaining box against that seed, freezes the group, and moves on:

idx = int(order[-1])
order = order[:-1]
ious = box_iou_batch(predictions[order][:, :4], predictions[idx : idx + 1, :4], overlap_metric)
above_threshold = ious >= iou_threshold
merge_group = [idx, *np.flip(order[above_threshold]).tolist()]

WBF matches each incoming box against the running fused box and recomputes that fused box every time a member is added, so the cluster centre drifts as it absorbs boxes. A box that misses the original seed but overlaps the drifted average joins in the paper's version and doesn't here, and cluster membership is order-dependent in a different way. On tight clusters the two agree; on spread-out ensemble output they won't.

Either implement the iterative form, or say plainly in the docstring that this is a greedy single-pass approximation of WBF rather than the algorithm from the paper. Both are defensible — silently differing from the reference is the option I'd avoid.

Minor

if group.shape[1] > 5:
    best_class = group[int(scores.argmax()), 5]

When there are 6 columns, _non_max_merge_per_category has already grouped per class, so every member of the group shares a class id and the argmax always resolves to the same value as group[0, 5]. Harmless, but the comment above it ("the class id is taken from the highest-scoring member") suggests mixed-class groups are possible on this path, and they aren't. Worth simplifying or rewording.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Its fixed-anchor grouping can produce different clusters from the standard iterative WBF algorithm.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces a public NumPy-based Weighted Box Fusion utility for combining overlapping detections.

Changes:

  • Adds box fusion logic and public API export.
  • Adds tests for fusion, class handling, ordering, and edge cases.
  • Adds API documentation and changelog entry.
File summaries
File Description
src/supervision/detection/utils/iou_and_nms.py Implements box fusion and grouping.
src/supervision/__init__.py Exports the new public API.
tests/detection/utils/test_iou_and_nms.py Adds WBF tests.
docs/detection/utils/iou_and_nms.md Adds generated API documentation.
docs/changelog.md Records the feature.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1649 to +1650
merge_groups = box_non_max_merge(predictions, iou_threshold, overlap_metric)
fused = np.stack([_fuse_box_group(predictions[group]) for group in merge_groups])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[weighted_box_fussion] - an alternative for box_non_max_suppression

4 participants