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
32 lines
1.2 KiB
Python
32 lines
1.2 KiB
Python
"""Where the eyes should look, given where faces are in the camera image. No ROS imports."""
|
|
from __future__ import annotations
|
|
|
|
from typing import NamedTuple, Optional, Sequence
|
|
|
|
|
|
class Target(NamedTuple):
|
|
cx: float # face centre, pixels
|
|
cy: float
|
|
w: float # face size, pixels
|
|
h: float
|
|
score: float = 1.0
|
|
name: str = '' # who, if recognised
|
|
|
|
|
|
def pick_target(faces: Sequence[Target]) -> Optional[Target]:
|
|
"""A recognised face wins over an unknown one; otherwise the biggest, most confident face."""
|
|
if not faces:
|
|
return None
|
|
return max(faces, key=lambda f: (bool(f.name), f.w * f.h * f.score))
|
|
|
|
|
|
def gaze_from_pixel(cx: float, cy: float, width: float, height: float) -> tuple[float, float]:
|
|
"""Image position -> eye direction (+x the viewer's right, +y down), each in [-1, 1].
|
|
|
|
The camera looks the way the robot faces, so a person on the right of the image is on the
|
|
robot's right, which is the viewer's left on the display: x flips, y does not.
|
|
"""
|
|
gx = (cx - width / 2.0) / (width / 2.0)
|
|
gy = (cy - height / 2.0) / (height / 2.0)
|
|
return -max(-1.0, min(1.0, gx)), max(-1.0, min(1.0, gy))
|