Hermes asked for structured detections instead of a bare JPEG. faces() returns the
faces in view with no photo: a persistent track id, bbox, detection confidence, how
long the person has been in view, and identity {name, similarity} or null, never a
made-up name. look() returns that same JSON as text plus the photo (send_image can
skip it); list_people() gives names and counts; robot://status and two prompts make
the server discoverable, where list_resources and list_prompts were empty. The
detector now tracks faces across frames (FaceTracker) and publishes the track id and
the identity as a second hypothesis.
Deliberately not built: embeddings in responses, age/gender attributes, auto-updating
face data, and a remote enroll tool (a prompt could make the robot call anyone "Kyle").
Checked end to end: a real Hermes turn on the local ornith model called faces() (0.03 s,
no image) and answered that Kyle was there at 0.78 similarity. Also: the camera is
calibrated (18 of 24 views, 0.296 px, outlier rejection added) and the calibration is
kept in pi/calibration and restored by the installer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
56 lines
1.9 KiB
Python
56 lines
1.9 KiB
Python
from hermes_vision.detect import Face
|
|
from hermes_vision.tracking import FaceTracker
|
|
|
|
|
|
def f(x, y=100, w=80, h=80):
|
|
return Face(x, y, w, h, 0.9)
|
|
|
|
|
|
def test_a_face_keeps_its_track_id_and_age_grows():
|
|
tr = FaceTracker()
|
|
ids = []
|
|
for i in range(8):
|
|
(t,) = tr.update([f(100 + 3 * i)], i * 0.25)
|
|
ids.append(t.id)
|
|
assert ids == [1] * 8 and abs(t.age(1.75) - 1.75) < 1e-9
|
|
|
|
|
|
def test_two_people_get_two_ids_and_keep_them_when_they_swap_order():
|
|
tr = FaceTracker()
|
|
a, b = tr.update([f(100), f(500)], 0.0)
|
|
b2, a2 = tr.update([f(502), f(102)], 0.25) # reported in the other order
|
|
assert (a.id, b.id) == (a2.id, b2.id) and a.id != b.id
|
|
|
|
|
|
def test_a_track_survives_a_short_gap_but_not_a_long_one():
|
|
tr = FaceTracker(max_age_s=1.5)
|
|
(t,) = tr.update([f(100)], 0.0)
|
|
tr.update([], 1.0) # nobody for a second
|
|
(t2,) = tr.update([f(100)], 1.2)
|
|
assert t2.id == t.id
|
|
tr.update([], 1.3)
|
|
tr.update([], 3.5) # gone for over max_age
|
|
(t3,) = tr.update([f(100)], 3.6)
|
|
assert t3.id != t.id
|
|
|
|
|
|
def test_identity_is_resolved_for_new_tracks_and_rechecked_once_a_second():
|
|
calls = []
|
|
tr = FaceTracker(recheck_s=1.0)
|
|
ident = lambda face: (calls.append(face), ('Kyle', 0.8))[1]
|
|
for i in range(8): # 4 Hz for two seconds
|
|
(t,) = tr.update([f(100 + i)], i * 0.25, ident)
|
|
assert t.name == 'Kyle' and t.similarity == 0.8
|
|
assert len(calls) == 2 # at t=0 and again at t=1.0
|
|
|
|
|
|
def test_a_stranger_is_unknown_not_named():
|
|
tr = FaceTracker()
|
|
(t,) = tr.update([f(100)], 0.0, lambda face: ('', 0.12))
|
|
assert t.name == '' and t.similarity == 0.12
|
|
|
|
|
|
def test_without_an_identifier_nothing_is_named():
|
|
(t,) = FaceTracker().update([f(100)], 0.0)
|
|
assert t.name == '' and t.id == 1
|