Skip to content

Commit 1d038bc

Browse files
authored
Merge branch 'main' into sbel/analytics-hierarchy
2 parents f87cec7 + ea56415 commit 1d038bc

File tree

5 files changed

+273
-30
lines changed

5 files changed

+273
-30
lines changed

autocalibration/requirements-runtime.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ scipy==1.17.1
1616
torch==2.11.0+cpu
1717
torchvision==0.26.0+cpu
1818
trimesh==4.11.5
19-
gdown==5.2.1
19+
gdown==5.2.2
2020
h5py==3.15.1
2121
matplotlib==3.10.8
2222
plotly==6.7.0

controller/src/controller/scene.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from scene_common import log
1212
from scene_common.camera import Camera
1313
from scene_common.earth_lla import convertLLAToECEF, calculateTRSLocal2LLAFromSurfacePoints
14-
from scene_common.geometry import Line, Point, Region, Tripwire
14+
from scene_common.geometry import Line, Point, Region, Tripwire, getRegionEvents, getTripwireEvents
1515
from scene_common.scene_model import SceneModel
1616
from scene_common.timestamp import get_epoch_time, get_iso_time
1717
from scene_common.transform import CameraPose
@@ -570,26 +570,33 @@ def _updateEvents(self, detectionType, now, curObjects=None):
570570
return
571571

572572
def _updateTripwireEvents(self, detectionType, now, curObjects):
573-
for key in self.tripwires:
574-
tripwire = self.tripwires[key]
575-
tripwireObjects = tripwire.objects.get(detectionType, [])
576-
objects = []
577-
for obj in curObjects:
578-
age = now - obj.when
579-
# When tracker is disabled, skip the frameCount check and consider all objects;
580-
# otherwise, only consider objects with frameCount > 3 as reliable.
581-
if (obj.frameCount > 3 or ControllerMode.isAnalyticsOnly()) \
582-
and len(obj.chain_data.publishedLocations) > 1:
583-
d = tripwire.lineCrosses(Line(obj.chain_data.publishedLocations[0].as2Dxy,
584-
obj.chain_data.publishedLocations[1].as2Dxy))
585-
if d != 0:
586-
event = TripwireEvent(obj, -d)
587-
objects.append(event)
588-
589-
if len(tripwireObjects) != len(objects) \
573+
# Filter to reliable objects with enough location history for crossing detection.
574+
# When tracker is disabled, skip the frameCount check and consider all objects;
575+
# otherwise, only consider objects with frameCount > 3 as reliable.
576+
reliable_objects = [
577+
obj for obj in curObjects
578+
if (obj.frameCount > 3 or ControllerMode.isAnalyticsOnly())
579+
and len(obj.chain_data.publishedLocations) > 1
580+
]
581+
582+
object_locations = [
583+
obj.chain_data.publishedLocations[:2] for obj in reliable_objects
584+
]
585+
586+
crossing_events = getTripwireEvents(self.tripwires, object_locations)
587+
588+
for key, tripwire in self.tripwires.items():
589+
event_matches = crossing_events.get(key, [])
590+
previous_objects = tripwire.objects.get(detectionType, [])
591+
crossed_objects = [
592+
TripwireEvent(reliable_objects[obj_idx], direction)
593+
for obj_idx, direction in event_matches
594+
]
595+
596+
if len(previous_objects) != len(crossed_objects) \
590597
and now - tripwire.when > DEBOUNCE_DELAY:
591-
log.debug("TRIPWIRE EVENT", tripwireObjects, len(objects))
592-
tripwire.objects[detectionType] = objects
598+
log.debug("TRIPWIRE EVENT", previous_objects, len(crossed_objects))
599+
tripwire.objects[detectionType] = crossed_objects
593600
tripwire.when = now
594601
if 'objects' not in self.events:
595602
self.events['objects'] = []
@@ -598,16 +605,27 @@ def _updateTripwireEvents(self, detectionType, now, curObjects):
598605

599606
def _updateRegionEvents(self, detectionType, regions, now, now_str, curObjects):
600607
updated = set()
601-
for key in regions:
602-
region = regions[key]
608+
609+
# Filter to reliable objects.
610+
# When tracker is disabled, skip the frameCount check and consider all objects;
611+
# otherwise, only consider objects with frameCount > 3 as reliable.
612+
reliable_objects = [
613+
obj for obj in curObjects
614+
if obj.frameCount > 3 or ControllerMode.isAnalyticsOnly()
615+
]
616+
617+
object_locations = [obj.sceneLoc for obj in reliable_objects]
618+
objects_within_region = getRegionEvents(regions, object_locations)
619+
620+
for key, region in regions.items():
621+
matched_indices = set(objects_within_region.get(key, []))
622+
# Also include objects matched by mesh intersection (requires self)
623+
for obj_idx, obj in enumerate(reliable_objects):
624+
if obj_idx not in matched_indices and self.isIntersecting(obj, region):
625+
matched_indices.add(obj_idx)
626+
627+
objects = [reliable_objects[i] for i in sorted(matched_indices)]
603628
regionObjects = region.objects.get(detectionType, [])
604-
objects = []
605-
for obj in curObjects:
606-
# When tracker is disabled, skip the frameCount check and consider all objects;
607-
# otherwise, only consider objects with frameCount > 3 as reliable.
608-
if (obj.frameCount > 3 or ControllerMode.isAnalyticsOnly()) \
609-
and (region.isPointWithin(obj.sceneLoc) or self.isIntersecting(obj, region)):
610-
objects.append(obj)
611629

612630
cur = set(x.gid for x in objects)
613631
prev = set(x.gid for x in regionObjects)

scene_common/src/scene_common/geometry.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,37 @@ def serialize(self):
194194
'uuid': self.uuid,
195195
}
196196
return data
197+
198+
def getTripwireEvents(tripwires, object_locations):
199+
"""Detect line crossings between object movement segments and tripwires.
200+
201+
@param tripwires Dict of {key: Tripwire} to check against
202+
@param object_locations List of location pairs (current, previous) per object
203+
@return Dict mapping tripwire key to list of (object_index, direction) tuples
204+
"""
205+
tripwire_events = {}
206+
for key, tripwire in tripwires.items():
207+
event_matches = []
208+
for obj_idx, obj_locations in enumerate(object_locations):
209+
d = tripwire.lineCrosses(Line(obj_locations[0].as2Dxy,
210+
obj_locations[1].as2Dxy))
211+
if d != 0:
212+
event_matches.append((obj_idx, -d))
213+
tripwire_events[key] = event_matches
214+
return tripwire_events
215+
216+
def getRegionEvents(regions, object_locations):
217+
"""Determine which objects are within each region using point containment.
218+
219+
@param regions Dict of {key: Region} to check against
220+
@param object_locations List of object positions (Point) to test
221+
@return Dict mapping region key to list of object indices within that region
222+
"""
223+
region_events = {}
224+
for key, region in regions.items():
225+
region_objects = []
226+
for obj_idx, obj_location in enumerate(object_locations):
227+
if region.isPointWithin(obj_location):
228+
region_objects.append(obj_idx)
229+
region_events[key] = region_objects
230+
return region_events
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# SPDX-FileCopyrightText: (C) 2026 Intel Corporation
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import math
5+
6+
import pytest
7+
8+
from scene_common.geometry import getRegionEvents, Region, Point
9+
10+
UUID = "39bd9698-8603-43fb-9cb9-06d9a14e6a24"
11+
12+
def _regular_polygon(n, cx=10, cy=10, r=10):
13+
"""! Generate vertices for a regular n-gon centered at (cx, cy) with radius r. """
14+
return [
15+
[cx + r * math.cos(2 * math.pi * i / n),
16+
cy + r * math.sin(2 * math.pi * i / n)]
17+
for i in range(n)
18+
]
19+
20+
# --- Positive tests: object inside various region types ---
21+
22+
@pytest.mark.parametrize("n_vertices", [3, 4, 5, 10],
23+
ids=["triangle", "quad", "pentagon", "decagon"])
24+
def test_object_inside_polygon(n_vertices):
25+
"""! Verify object at polygon center is detected inside for varying vertex counts. """
26+
pts = _regular_polygon(n_vertices)
27+
region = Region(UUID, "poly", {"points": pts})
28+
result = getRegionEvents({"r": region}, [Point(10, 10)])
29+
assert result["r"] == [0]
30+
31+
def test_object_inside_circle():
32+
"""! Verify object at circle center is detected inside. """
33+
region = Region(UUID, "circle", {"area": "circle", "center": [5, 5], "radius": 3})
34+
result = getRegionEvents({"r": region}, [Point(5, 5)])
35+
assert result["r"] == [0]
36+
37+
def test_object_inside_scene():
38+
"""! Verify any object is inside a scene-wide region. """
39+
region = Region(UUID, "scene", {"area": "scene"})
40+
result = getRegionEvents({"r": region}, [Point(100, 200)])
41+
assert result["r"] == [0]
42+
43+
# --- No-match tests: object outside all regions ---
44+
45+
def test_no_match_outside_polygon():
46+
"""! Verify object outside polygon is not detected. """
47+
region = Region(UUID, "poly", {"points": [[0, 0], [10, 0], [10, 10], [0, 10]]})
48+
result = getRegionEvents({"r": region}, [Point(50, 50)])
49+
assert result["r"] == []
50+
51+
def test_no_match_outside_circle():
52+
"""! Verify object outside circle is not detected. """
53+
region = Region(UUID, "circle", {"area": "circle", "center": [5, 5], "radius": 3})
54+
result = getRegionEvents({"r": region}, [Point(50, 50)])
55+
assert result["r"] == []
56+
57+
# --- Length variations ---
58+
59+
def test_single_region_single_object():
60+
"""! Verify single region with one matching object. """
61+
region = Region(UUID, "poly", {"points": [[0, 0], [10, 0], [10, 10], [0, 10]]})
62+
result = getRegionEvents({"r": region}, [Point(5, 5)])
63+
assert result["r"] == [0]
64+
65+
def test_multiple_regions_multiple_objects():
66+
"""! Verify correct mapping with overlapping regions and multiple objects. """
67+
region_a = Region(UUID, "a", {"points": [[0, 0], [10, 0], [10, 10], [0, 10]]})
68+
region_b = Region(UUID, "b", {"points": [[5, 5], [15, 5], [15, 15], [5, 15]]})
69+
region_c = Region(UUID, "c", {"area": "circle", "center": [20, 20], "radius": 2})
70+
regions = {"a": region_a, "b": region_b, "c": region_c}
71+
objects = [Point(7, 7), Point(2, 2), Point(20, 20), Point(50, 50)]
72+
result = getRegionEvents(regions, objects)
73+
assert sorted(result["a"]) == [0, 1]
74+
assert result["b"] == [0]
75+
assert result["c"] == [2]
76+
77+
# --- Empty inputs ---
78+
79+
def test_empty_object_list():
80+
"""! Verify regions with no objects returns empty lists per key. """
81+
region = Region(UUID, "poly", {"points": [[0, 0], [10, 0], [10, 10], [0, 10]]})
82+
result = getRegionEvents({"r": region}, [])
83+
assert result["r"] == []
84+
85+
def test_empty_region_dict():
86+
"""! Verify empty region dict returns empty result. """
87+
result = getRegionEvents({}, [Point(5, 5)])
88+
assert result == {}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# SPDX-FileCopyrightText: (C) 2026 Intel Corporation
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import pytest
5+
6+
from scene_common.geometry import getTripwireEvents, Tripwire, Point
7+
8+
UUID = "39bd9698-8603-43fb-9cb9-06d9a14e6a24"
9+
10+
# --- Positive tests: crossing in both directions for four orientations ---
11+
12+
@pytest.mark.parametrize(
13+
"tripwire_pts, current, previous, expected_dir",
14+
[
15+
# Horizontal tripwire [(0,0) -> (10,0)]
16+
([[0, 0], [10, 0]], Point(5, 1), Point(5, -1), -1),
17+
([[0, 0], [10, 0]], Point(5, -1), Point(5, 1), 1),
18+
# Vertical tripwire [(5,0) -> (5,10)]
19+
([[5, 0], [5, 10]], Point(6, 5), Point(4, 5), 1),
20+
([[5, 0], [5, 10]], Point(4, 5), Point(6, 5), -1),
21+
# Acute angle (~45 deg) tripwire [(0,0) -> (10,10)]
22+
([[0, 0], [10, 10]], Point(10, 0), Point(0, 10), 1),
23+
([[0, 0], [10, 10]], Point(0, 10), Point(10, 0), -1),
24+
# Obtuse angle (~135 deg) tripwire [(10,0) -> (0,10)]
25+
([[10, 0], [0, 10]], Point(0, 0), Point(10, 10), -1),
26+
([[10, 0], [0, 10]], Point(10, 10), Point(0, 0), 1),
27+
],
28+
ids=[
29+
"horizontal-south-to-north",
30+
"horizontal-north-to-south",
31+
"vertical-left-to-right",
32+
"vertical-right-to-left",
33+
"acute-cross-a",
34+
"acute-cross-b",
35+
"obtuse-cross-a",
36+
"obtuse-cross-b",
37+
])
38+
def test_tripwire_crossing(tripwire_pts, current, previous, expected_dir):
39+
"""! Verify tripwire crossing detection for various orientations and directions. """
40+
tripwire = Tripwire(UUID, "tw", {"points": tripwire_pts})
41+
result = getTripwireEvents({"tw": tripwire}, [(current, previous)])
42+
assert len(result["tw"]) == 1
43+
assert result["tw"][0] == (0, expected_dir)
44+
45+
# --- No-match tests ---
46+
47+
def test_no_crossing_parallel_offset():
48+
"""! Verify no crossing when movement is parallel to tripwire but offset. """
49+
tripwire = Tripwire(UUID, "tw", {"points": [[0, 0], [10, 0]]})
50+
result = getTripwireEvents({"tw": tripwire}, [(Point(15, 2), Point(5, 2))])
51+
assert result["tw"] == []
52+
53+
def test_no_crossing_collinear():
54+
"""! Verify no crossing when movement is along the tripwire (collinear). """
55+
tripwire = Tripwire(UUID, "tw", {"points": [[0, 0], [10, 0]]})
56+
result = getTripwireEvents({"tw": tripwire}, [(Point(8, 0), Point(2, 0))])
57+
assert result["tw"] == []
58+
59+
# --- Positive tests: endpoint on tripwire ---
60+
61+
def test_crossing_when_endpoint_on_tripwire():
62+
"""! Verify that endpoint landing exactly on the tripwire counts as a crossing. """
63+
tripwire = Tripwire(UUID, "tw", {"points": [[0, 0], [10, 0]]})
64+
result = getTripwireEvents({"tw": tripwire}, [(Point(5, 0), Point(5, -5))])
65+
assert len(result["tw"]) == 1
66+
assert result["tw"][0][0] == 0
67+
68+
# --- Length variations ---
69+
70+
def test_single_tripwire_single_object():
71+
"""! Verify single tripwire with one crossing object. """
72+
tripwire = Tripwire(UUID, "tw", {"points": [[0, 0], [10, 0]]})
73+
result = getTripwireEvents({"tw": tripwire}, [(Point(5, 1), Point(5, -1))])
74+
assert len(result["tw"]) == 1
75+
assert result["tw"][0][0] == 0
76+
77+
def test_multiple_tripwires_multiple_objects():
78+
"""! Verify detection with multiple tripwires and objects, one crossing both. """
79+
tw_h = Tripwire(UUID, "h", {"points": [[0, 0], [10, 0]]})
80+
tw_v = Tripwire(UUID, "v", {"points": [[5, -5], [5, 5]]})
81+
tripwires = {"h": tw_h, "v": tw_v}
82+
objects = [
83+
(Point(6, 1), Point(4, -1)), # crosses both
84+
(Point(1, 5), Point(1, 4)), # crosses neither
85+
]
86+
result = getTripwireEvents(tripwires, objects)
87+
assert len(result["h"]) == 1
88+
assert result["h"][0][0] == 0
89+
assert len(result["v"]) == 1
90+
assert result["v"][0][0] == 0
91+
92+
# --- Empty inputs ---
93+
94+
def test_empty_object_list():
95+
"""! Verify tripwires with no objects returns empty lists per key. """
96+
tripwire = Tripwire(UUID, "tw", {"points": [[0, 0], [10, 0]]})
97+
result = getTripwireEvents({"tw": tripwire}, [])
98+
assert result["tw"] == []
99+
100+
def test_empty_tripwire_dict():
101+
"""! Verify empty tripwire dict returns empty result. """
102+
result = getTripwireEvents({}, [(Point(5, 1), Point(5, -1))])
103+
assert result == {}

0 commit comments

Comments
 (0)