fix: replace deprecated 2-D np.cross with explicit determinant (#2386)

- Add filterwarnings = ["error::DeprecationWarning"] to pyproject.toml so
  future np.cross 2-D reintroductions fail CI immediately (closes #2384)
- Add test_get_polygon_center_no_deprecation_warning: asserts no
  DeprecationWarning from get_polygon_center (Copilot inline comment)
- Add test_cross_product_no_deprecation_warning: asserts no DeprecationWarning
  from cross_product (Copilot inline comment)
- Add test_cross_product_sign (4 parametrised cases): above / below / on-line /
  offset-start — directly tests the inline determinant correctness
- Improve cross_product docstring: blank line after summary, adds Examples
  section with correct output, notes NumPy 2.0 rationale

---------

Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
Abhijith Neil Abraham 2026-07-02 10:01:39 -07:00 committed by GitHub
parent 99049d84e1
commit f196e15f26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 129 additions and 7 deletions

View File

@ -199,6 +199,9 @@ ini_options.addopts = [
"--doctest-modules",
"--color=yes",
]
ini_options.filterwarnings = [
"error::DeprecationWarning",
]
ini_options.doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE"
[tool.autoflake]

View File

@ -653,14 +653,28 @@ def get_data_item(
def cross_product(
anchors: npt.NDArray[np.number], vector: Vector
) -> npt.NDArray[np.number]:
"""
Get array of cross products of each anchor with a vector.
"""Get signed z-component of cross product (2-D determinant) per anchor.
Replaces the deprecated `np.cross` 2-D path (NumPy 2.0) with an explicit
determinant: ``a[..., 0] * b[..., 1] - a[..., 1] * b[..., 0]``.
Args:
anchors: Array of anchors of shape (number of anchors, detections, 2)
vector: Vector to calculate cross product with
anchors: Array of anchors of shape (number of anchors, detections, 2).
vector: Vector to calculate cross product with.
Returns:
Array of cross products of shape (number of anchors, detections)
Array of signed cross-product values, shape (number of anchors,
detections). Positive = anchor is to the left of the vector direction;
negative = to the right; zero = on the line.
Examples:
>>> import numpy as np
>>> from supervision.geometry.core import Point, Vector
>>> anchors = np.array([[[10.0, 5.0]], [[20.0, 5.0]]])
>>> vector = Vector(start=Point(0, 0), end=Point(1, 0))
>>> cross_product(anchors, vector)
array([[5.],
[5.]])
"""
vector_at_zero = np.array(
[
@ -669,6 +683,8 @@ def cross_product(
]
)
vector_start = np.array([vector.start.x, vector.start.y])
diff = anchors - vector_start
return cast(
npt.NDArray[np.number], np.cross(vector_at_zero, anchors - vector_start)
npt.NDArray[np.number],
vector_at_zero[0] * diff[..., 1] - vector_at_zero[1] * diff[..., 0],
)

View File

@ -41,7 +41,10 @@ def get_polygon_center(polygon: npt.NDArray[np.float64]) -> Point:
raise ValueError("Polygon must have at least one vertex.")
shift_polygon = np.roll(polygon, -1, axis=0)
signed_areas = np.cross(polygon, shift_polygon) / 2
signed_areas = (
polygon[..., 0] * shift_polygon[..., 1]
- polygon[..., 1] * shift_polygon[..., 0]
) / 2
if signed_areas.sum() == 0:
center = np.mean(polygon, axis=0).round()
return Point(x=center[0], y=center[1])

View File

@ -879,6 +879,26 @@ def test_line_zone_tracker_id_reuse_with_different_classes(
assert line_zone.out_count_per_class == expected_out_count_per_class
def test_line_zone_trigger_does_not_call_np_cross(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Guard against reintroducing np.cross, deprecated for 2-D input in NumPy 2.0."""
def _raise(*args, **kwargs):
raise AssertionError("np.cross must not be called on 2-D vectors")
monkeypatch.setattr(np, "cross", _raise)
line_zone = LineZone(start=Point(0, 0), end=Point(0, 10))
for xyxy in [[4, 4, 6, 6], [-6, 4, -4, 6]]:
detections = _create_detections(xyxy=[xyxy], tracker_id=[0])
crossed_in, crossed_out = line_zone.trigger(detections)
assert not crossed_in[0]
assert crossed_out[0]
assert line_zone.out_count == 1
def test_line_zone_annotator_multiclass_supports_none_class_id() -> None:
line_zone = LineZone(start=Point(0, 0), end=Point(0, 10))
for xyxy in [[4, 4, 6, 6], [-6, 4, -4, 6]]:

View File

@ -1125,3 +1125,58 @@ def test_process_roboflow_result_compact_masks_rle_mask_size_mismatch() -> None:
assert isinstance(compact_result[3], CompactMask)
np.testing.assert_array_equal(compact_result[3].to_dense(), dense_result[3])
# ---------------------------------------------------------------------------
# cross_product — regression + unit tests (GitHub #2384)
# ---------------------------------------------------------------------------
import warnings # noqa: E402
from supervision.detection.utils.internal import cross_product # noqa: E402
from supervision.geometry.core import Point, Vector # noqa: E402
def test_cross_product_no_deprecation_warning() -> None:
"""Regression for #2384: cross_product must not fire DeprecationWarning."""
anchors = np.array([[[5.0, 5.0]]])
v = Vector(start=Point(0, 0), end=Point(10, 0))
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
cross_product(anchors, v)
@pytest.mark.parametrize(
("anchors", "vector", "expected_sign"),
[
pytest.param(
np.array([[[5.0, 5.0]]]),
Vector(Point(0, 0), Point(10, 0)),
1,
id="above",
),
pytest.param(
np.array([[[5.0, -5.0]]]),
Vector(Point(0, 0), Point(10, 0)),
-1,
id="below",
),
pytest.param(
np.array([[[5.0, 0.0]]]),
Vector(Point(0, 0), Point(10, 0)),
0,
id="on-line",
),
pytest.param(
np.array([[[3.0, 3.0]]]),
Vector(Point(1, 1), Point(5, 1)),
1,
id="offset-start",
),
],
)
def test_cross_product_sign(
anchors: np.ndarray, vector: Vector, expected_sign: int
) -> None:
"""Verify cross_product returns correct sign for known anchor/vector pairs."""
result = cross_product(anchors, vector)
assert int(np.sign(result[0, 0])) == expected_sign

View File

@ -58,3 +58,28 @@ def test_get_polygon_center(polygon: np.ndarray, expected_result: Point) -> None
"""
result = get_polygon_center(polygon)
assert result == expected_result
def test_get_polygon_center_no_deprecation_warning() -> None:
"""Regression for #2384: get_polygon_center must not fire DeprecationWarning."""
import warnings
polygon = np.array([[0, 0], [0, 2], [2, 2], [2, 0]], dtype=float)
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
get_polygon_center(polygon=polygon)
def test_get_polygon_center_does_not_call_np_cross(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Guard against reintroducing np.cross, deprecated for 2-D input in NumPy 2.0."""
def _raise(*args, **kwargs):
raise AssertionError("np.cross must not be called on 2-D vectors")
monkeypatch.setattr(np, "cross", _raise)
result = get_polygon_center(generate_test_polygon(100))
assert result == Point(x=50.0, y=121.0)