Skip to content

Commit f196e15

Browse files
abhijithneilabrahamBordaclaude[bot]
authored
fix: replace deprecated 2-D np.cross with explicit determinant (roboflow#2386)
- Add filterwarnings = ["error::DeprecationWarning"] to pyproject.toml so future np.cross 2-D reintroductions fail CI immediately (closes roboflow#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>
1 parent 99049d8 commit f196e15

6 files changed

Lines changed: 129 additions & 7 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ ini_options.addopts = [
199199
"--doctest-modules",
200200
"--color=yes",
201201
]
202+
ini_options.filterwarnings = [
203+
"error::DeprecationWarning",
204+
]
202205
ini_options.doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE"
203206

204207
[tool.autoflake]

src/supervision/detection/utils/internal.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -653,14 +653,28 @@ def get_data_item(
653653
def cross_product(
654654
anchors: npt.NDArray[np.number], vector: Vector
655655
) -> npt.NDArray[np.number]:
656-
"""
657-
Get array of cross products of each anchor with a vector.
656+
"""Get signed z-component of cross product (2-D determinant) per anchor.
657+
658+
Replaces the deprecated `np.cross` 2-D path (NumPy 2.0) with an explicit
659+
determinant: ``a[..., 0] * b[..., 1] - a[..., 1] * b[..., 0]``.
660+
658661
Args:
659-
anchors: Array of anchors of shape (number of anchors, detections, 2)
660-
vector: Vector to calculate cross product with
662+
anchors: Array of anchors of shape (number of anchors, detections, 2).
663+
vector: Vector to calculate cross product with.
661664
662665
Returns:
663-
Array of cross products of shape (number of anchors, detections)
666+
Array of signed cross-product values, shape (number of anchors,
667+
detections). Positive = anchor is to the left of the vector direction;
668+
negative = to the right; zero = on the line.
669+
670+
Examples:
671+
>>> import numpy as np
672+
>>> from supervision.geometry.core import Point, Vector
673+
>>> anchors = np.array([[[10.0, 5.0]], [[20.0, 5.0]]])
674+
>>> vector = Vector(start=Point(0, 0), end=Point(1, 0))
675+
>>> cross_product(anchors, vector)
676+
array([[5.],
677+
[5.]])
664678
"""
665679
vector_at_zero = np.array(
666680
[
@@ -669,6 +683,8 @@ def cross_product(
669683
]
670684
)
671685
vector_start = np.array([vector.start.x, vector.start.y])
686+
diff = anchors - vector_start
672687
return cast(
673-
npt.NDArray[np.number], np.cross(vector_at_zero, anchors - vector_start)
688+
npt.NDArray[np.number],
689+
vector_at_zero[0] * diff[..., 1] - vector_at_zero[1] * diff[..., 0],
674690
)

src/supervision/geometry/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ def get_polygon_center(polygon: npt.NDArray[np.float64]) -> Point:
4141
raise ValueError("Polygon must have at least one vertex.")
4242

4343
shift_polygon = np.roll(polygon, -1, axis=0)
44-
signed_areas = np.cross(polygon, shift_polygon) / 2
44+
signed_areas = (
45+
polygon[..., 0] * shift_polygon[..., 1]
46+
- polygon[..., 1] * shift_polygon[..., 0]
47+
) / 2
4548
if signed_areas.sum() == 0:
4649
center = np.mean(polygon, axis=0).round()
4750
return Point(x=center[0], y=center[1])

tests/detection/test_line_counter.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,26 @@ def test_line_zone_tracker_id_reuse_with_different_classes(
879879
assert line_zone.out_count_per_class == expected_out_count_per_class
880880

881881

882+
def test_line_zone_trigger_does_not_call_np_cross(
883+
monkeypatch: pytest.MonkeyPatch,
884+
) -> None:
885+
"""Guard against reintroducing np.cross, deprecated for 2-D input in NumPy 2.0."""
886+
887+
def _raise(*args, **kwargs):
888+
raise AssertionError("np.cross must not be called on 2-D vectors")
889+
890+
monkeypatch.setattr(np, "cross", _raise)
891+
892+
line_zone = LineZone(start=Point(0, 0), end=Point(0, 10))
893+
for xyxy in [[4, 4, 6, 6], [-6, 4, -4, 6]]:
894+
detections = _create_detections(xyxy=[xyxy], tracker_id=[0])
895+
crossed_in, crossed_out = line_zone.trigger(detections)
896+
897+
assert not crossed_in[0]
898+
assert crossed_out[0]
899+
assert line_zone.out_count == 1
900+
901+
882902
def test_line_zone_annotator_multiclass_supports_none_class_id() -> None:
883903
line_zone = LineZone(start=Point(0, 0), end=Point(0, 10))
884904
for xyxy in [[4, 4, 6, 6], [-6, 4, -4, 6]]:

tests/detection/utils/test_internal.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,3 +1125,58 @@ def test_process_roboflow_result_compact_masks_rle_mask_size_mismatch() -> None:
11251125

11261126
assert isinstance(compact_result[3], CompactMask)
11271127
np.testing.assert_array_equal(compact_result[3].to_dense(), dense_result[3])
1128+
1129+
1130+
# ---------------------------------------------------------------------------
1131+
# cross_product — regression + unit tests (GitHub #2384)
1132+
# ---------------------------------------------------------------------------
1133+
import warnings # noqa: E402
1134+
1135+
from supervision.detection.utils.internal import cross_product # noqa: E402
1136+
from supervision.geometry.core import Point, Vector # noqa: E402
1137+
1138+
1139+
def test_cross_product_no_deprecation_warning() -> None:
1140+
"""Regression for #2384: cross_product must not fire DeprecationWarning."""
1141+
anchors = np.array([[[5.0, 5.0]]])
1142+
v = Vector(start=Point(0, 0), end=Point(10, 0))
1143+
with warnings.catch_warnings():
1144+
warnings.simplefilter("error", DeprecationWarning)
1145+
cross_product(anchors, v)
1146+
1147+
1148+
@pytest.mark.parametrize(
1149+
("anchors", "vector", "expected_sign"),
1150+
[
1151+
pytest.param(
1152+
np.array([[[5.0, 5.0]]]),
1153+
Vector(Point(0, 0), Point(10, 0)),
1154+
1,
1155+
id="above",
1156+
),
1157+
pytest.param(
1158+
np.array([[[5.0, -5.0]]]),
1159+
Vector(Point(0, 0), Point(10, 0)),
1160+
-1,
1161+
id="below",
1162+
),
1163+
pytest.param(
1164+
np.array([[[5.0, 0.0]]]),
1165+
Vector(Point(0, 0), Point(10, 0)),
1166+
0,
1167+
id="on-line",
1168+
),
1169+
pytest.param(
1170+
np.array([[[3.0, 3.0]]]),
1171+
Vector(Point(1, 1), Point(5, 1)),
1172+
1,
1173+
id="offset-start",
1174+
),
1175+
],
1176+
)
1177+
def test_cross_product_sign(
1178+
anchors: np.ndarray, vector: Vector, expected_sign: int
1179+
) -> None:
1180+
"""Verify cross_product returns correct sign for known anchor/vector pairs."""
1181+
result = cross_product(anchors, vector)
1182+
assert int(np.sign(result[0, 0])) == expected_sign

tests/geometry/test_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,28 @@ def test_get_polygon_center(polygon: np.ndarray, expected_result: Point) -> None
5858
"""
5959
result = get_polygon_center(polygon)
6060
assert result == expected_result
61+
62+
63+
def test_get_polygon_center_no_deprecation_warning() -> None:
64+
"""Regression for #2384: get_polygon_center must not fire DeprecationWarning."""
65+
import warnings
66+
67+
polygon = np.array([[0, 0], [0, 2], [2, 2], [2, 0]], dtype=float)
68+
with warnings.catch_warnings():
69+
warnings.simplefilter("error", DeprecationWarning)
70+
get_polygon_center(polygon=polygon)
71+
72+
73+
def test_get_polygon_center_does_not_call_np_cross(
74+
monkeypatch: pytest.MonkeyPatch,
75+
) -> None:
76+
"""Guard against reintroducing np.cross, deprecated for 2-D input in NumPy 2.0."""
77+
78+
def _raise(*args, **kwargs):
79+
raise AssertionError("np.cross must not be called on 2-D vectors")
80+
81+
monkeypatch.setattr(np, "cross", _raise)
82+
83+
result = get_polygon_center(generate_test_polygon(100))
84+
85+
assert result == Point(x=50.0, y=121.0)

0 commit comments

Comments
 (0)