Conversation
…fusion # Conflicts: # docs/changelog.md
amerob
left a comment
There was a problem hiding this comment.
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_suppression → box_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.
There was a problem hiding this comment.
🟡 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.
| merge_groups = box_non_max_merge(predictions, iou_threshold, overlap_metric) | ||
| fused = np.stack([_fuse_box_group(predictions[group]) for group in merge_groups]) |
Before submitting
Description
Adds
sv.box_weighted_box_fusion, an implementation of Weighted Box Fusion (WBF; Solovyev et al., 2019) as a confidence-weighted alternative tosv.box_non_max_suppression. Where NMS discards overlapping boxes andbox_non_max_mergeonly 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
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) andbox_iou_batch, so it stays consistent with the other functions iniou_and_nms.pyand 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 optionalweightsor model-count parameter, or summing then capping the confidence), I am glad to adjust.Changes Made
sv.box_weighted_box_fusionindetection/utils/iou_and_nms.py, with a private_fuse_box_grouphelper 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.supervision/__init__.py.docs/detection/utils/iou_and_nms.md.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.pypass, including the 9 new ones;ruffandmypyare clean on the new code, and the docstring doctest passes. Numeric check: for[0, 0, 10, 10]at score0.9and[1, 1, 11, 11]at score0.8(IoU 0.68), the fused box is[0.471, 0.471, 10.471, 10.471]at score0.85. Since this is a deterministic pure-NumPy function, the unit tests serve as the reproduction rather than a Colab.