Skip to content

fix(iou): clamp box extents so a malformed box cannot earn a GIoU bonus - #589

Open
MahathirMohammadShuvo wants to merge 1 commit into
roboflow:developfrom
MahathirMohammadShuvo:fix/giou-malformed-box-area-clamp
Open

fix(iou): clamp box extents so a malformed box cannot earn a GIoU bonus#589
MahathirMohammadShuvo wants to merge 1 commit into
roboflow:developfrom
MahathirMohammadShuvo:fix/giou-malformed-box-area-clamp

Conversation

@MahathirMohammadShuvo

Copy link
Copy Markdown

The defect

_compute_iou_and_enclosing computes box area as (x2 - x1) * (y2 - y1) with no clamp — three lines after the intersection is correctly clamped with np.maximum(..., 0):

intersection = np.maximum(inter_x2 - inter_x1, 0) * np.maximum(inter_y2 - inter_y1, 0)   # clamped

area_1 = (boxes_1[:, 2] - boxes_1[:, 0]) * (boxes_1[:, 3] - boxes_1[:, 1])               # not clamped

For a box inverted on both axes the two negative extents multiply to a positive area. That inflates union past enclosing_area, so GIoU's (enclosing - union) / enclosing penalty term goes negative — a bonus instead of a penalty.

Reachable through a shipped tracker

SORTTracker's default XYXYStateEstimator has an unconstrained velocity (clamp_velocity is a documented no-op — "XYXY-style velocity is unconstrained"), and sort/tracklet.py has no downstream box clamp: state_to_bbox() returns kf.state[:4] straight into self.iou.compute.

Four shrinking detections, then six unmatched frames:

tracker = SORTTracker(iou=GIoU())
# ... tracklet bbox -> [274.3, 274.3, 125.7, 125.7]     x2 < x1 and y2 < y1
GIoU(ghost box, real detection) GIoU(well-formed near-perfect match)
before +2.2080 +0.6400
after +0.0000 +0.6400

The ghost outranks a genuine match, at a score outside GIoU's documented [-1, 1]. Over 3,000 random malformed pairs the pre-fix maximum was +29.54.

Scope

  • GIoU only. IoU/BIoU route through sv.box_iou_batch and never enter this helper. DIoU/CIoU are byte-identical before and after, because a malformed axis already forces intersection = 0 and they consume only iou and enclosing_diagonal_sq.
  • Well-formed results are bit-identicalnp.maximum(x, 0) returns x unchanged for x > 0 — so no benchmark run is needed.
  • Not fixed here: DIoU/CIoU remain unbounded below on malformed input, because the enclosing box is built from min/max of the corners, so an inverted box makes C stop enclosing the pair and the distance normaliser collapses ([500,500,0,0] vs [0,0,10,10] → DIoU -600.25). That is a separate defect in a different term. The docstrings now say so rather than implying it is covered — happy to fold it into this PR if you would prefer.
  • I first tried guarding the producing estimators (XYXYStateEstimator / XCYCWHStateEstimator both have no-op clamp_velocity) and abandoned it: freezing extent velocity changes tracking output on the default frame_step=1.0 path for BoT-SORT/McByte/C-BIoU, and can freeze the velocity that would recover an inverted box. Fixing the metric covers every producer at once.

Tests

TestDegenerateInputs.test_inverted_coords_gives_zero_or_negative_similarity promised its name but asserted only shape and finiteness, so it passed at GIoU +1.0. It is now split into an exact-value test (all three inversion patterns must equal a well-formed zero-area box) and a sign test across IoU/GIoU/DIoU/CIoU/BIoU × 3 patterns, plus a union <= enclosing_area invariant.

5 assertions fail on develop without the source change, including assert 4000.0 <= 3200.0.

ruff check clean · ruff format clean · mypy clean · iou.py doctests 6/6 · tests/utils + tests/motion 165 passed · tests/core fast subset 607 passed.

_compute_iou_and_enclosing computed box area as (x2 - x1) * (y2 - y1)
without clamping, three lines after the intersection is correctly
clamped with np.maximum(..., 0). For a box inverted on both axes the two
negative extents multiply to a positive area, which inflates the union
past the enclosing area and flips GIoU's (enclosing - union) / enclosing
penalty into a bonus.

Reachable through SORTTracker, whose default XYXYStateEstimator has an
unconstrained velocity and whose tracklet has no downstream box clamp.
A shrinking track left unmatched for six frames yields the bbox
[274.3, 274.3, 125.7, 125.7], which scored GIoU +2.2080 against a real
detection versus +0.6400 for a well-formed near-perfect match, outside
GIoU's documented [-1, 1] range. It now scores 0.0.

Affects GIoU only: IoU and BIoU route through sv.box_iou_batch and never
enter this helper, and DIoU and CIoU are byte-identical because a
malformed axis already forces the intersection to zero. Results for
well-formed boxes are bit-identical, so no benchmark run is required.

Extends TestDegenerateInputs, whose inverted-coordinate test previously
asserted only shape and finiteness and so passed at GIoU +1.0.
@CLAassistant

CLAassistant commented Sep 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants