Make the eyes follow detected faces; start the face at power-up
hermes_face now subscribes to /vision/faces and glances toward the best face (a recognised one first, then the biggest), smoothed, holding 2.5 s and drifting back to centre when none is seen. Sleeping ignores gaze. Image x flips on the display: the camera looks the way the robot faces, so a person on the image's right is on the robot's right, the viewer's left. Fix the pan sign carried over from the prototype: positive pan is the robot's left (the MCP contract), which is the viewer's right on the display. robot-face.service starts the face at boot and restarts it on failure, with a private working directory because lgpio drops a notification pipe in its cwd and systemd's default / is not writable. robot-cpu-performance.service holds the CPU at full clock, which halves the face's CPU cost. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
from hermes_face.gaze import Target, gaze_from_pixel, pick_target
|
||||
|
||||
|
||||
def test_centre_is_straight_ahead():
|
||||
assert gaze_from_pixel(648, 486, 1296, 972) == (0.0, 0.0)
|
||||
|
||||
|
||||
def test_person_on_the_image_right_is_the_viewers_left_on_the_display():
|
||||
gx, gy = gaze_from_pixel(1296, 486, 1296, 972)
|
||||
assert gx == -1.0 and gy == 0.0
|
||||
|
||||
|
||||
def test_person_high_in_the_image_is_up_on_the_display():
|
||||
assert gaze_from_pixel(648, 0, 1296, 972) == (0.0, -1.0)
|
||||
|
||||
|
||||
def test_off_image_positions_are_clamped():
|
||||
assert gaze_from_pixel(5000, -5000, 1296, 972) == (-1.0, -1.0)
|
||||
|
||||
|
||||
def test_pick_target_prefers_a_recognised_face_then_the_biggest():
|
||||
small_known = Target(100, 100, 50, 60, 0.9, 'owner')
|
||||
big_unknown = Target(500, 400, 300, 400, 0.9, '')
|
||||
mid_unknown = Target(300, 300, 200, 250, 0.9, '')
|
||||
assert pick_target([big_unknown, small_known]) is small_known
|
||||
assert pick_target([mid_unknown, big_unknown]) is big_unknown
|
||||
assert pick_target([]) is None
|
||||
@@ -60,7 +60,7 @@ def test_pan_moves_the_eyes():
|
||||
frame, _ = run(r, 2)
|
||||
lum = frame.astype(np.float32).sum(axis=(0, 2))
|
||||
return float((lum * np.arange(W)).sum() / lum.sum())
|
||||
assert centroid_x(-60) > centroid_x(0) > centroid_x(60) # +pan = robot's left = viewer's right
|
||||
assert centroid_x(60) > centroid_x(0) > centroid_x(-60) # +pan = robot's left = viewer's right
|
||||
|
||||
|
||||
def test_box_blur_conserves_energy():
|
||||
@@ -110,3 +110,37 @@ def test_wild_parameters_are_clamped():
|
||||
def test_unknown_phosphor_rejected():
|
||||
with pytest.raises(ValueError):
|
||||
FaceRenderer(phosphor='P99')
|
||||
|
||||
|
||||
def eye_centroid(r, seconds, t0=0.0):
|
||||
frame, t = run(r, seconds, t0)
|
||||
lum = frame[:130].astype(np.float32).sum(axis=(0, 2)) # the eyes live in the top half
|
||||
rows = frame[:130].astype(np.float32).sum(axis=(1, 2))
|
||||
return float((lum * np.arange(W)).sum() / lum.sum()), float((rows * np.arange(130)).sum() / rows.sum()), t
|
||||
|
||||
|
||||
def test_gaze_moves_the_eyes_toward_the_person_and_drifts_back():
|
||||
r = FaceRenderer(state='idle', seed=0, jitter=0)
|
||||
cx0, cy0, t = eye_centroid(r, 2)
|
||||
r.set_gaze(1.0, 0.0, t)
|
||||
cx1, _, t = eye_centroid(r, 1, t) # held, so still glancing
|
||||
assert cx1 > cx0 + 5
|
||||
r.set_gaze(0.0, -1.0, t)
|
||||
_, cy2, t = eye_centroid(r, 1, t)
|
||||
assert cy2 < cy0 - 3 # up on the display
|
||||
cx3, cy3, _ = eye_centroid(r, 8, t) # no refresh: hold expires, eyes return
|
||||
assert abs(cx3 - cx0) < 2 and abs(cy3 - cy0) < 2
|
||||
|
||||
|
||||
def test_sleeping_ignores_gaze():
|
||||
r = FaceRenderer(state='sleeping', seed=0, jitter=0)
|
||||
cx0, _, t = eye_centroid(r, 4)
|
||||
r.set_gaze(1.0, 0.0, t)
|
||||
cx1, _, _ = eye_centroid(r, 2, t)
|
||||
assert abs(cx1 - cx0) < 1.5
|
||||
|
||||
|
||||
def test_gaze_is_clamped():
|
||||
r = FaceRenderer(seed=0)
|
||||
r.set_gaze(50, -50, 0.0)
|
||||
assert r._gaze == (1.0, -1.0)
|
||||
|
||||
Reference in New Issue
Block a user