From b8f71f6bee2a2282c1a815c97689c1f81780357a Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Sun, 4 Jan 2026 18:56:04 +0100 Subject: [PATCH] feat: base face detection ref: N25B-397 --- .../endpoints/face_detector.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/robot_interface/endpoints/face_detector.py diff --git a/src/robot_interface/endpoints/face_detector.py b/src/robot_interface/endpoints/face_detector.py new file mode 100644 index 0000000..b25f656 --- /dev/null +++ b/src/robot_interface/endpoints/face_detector.py @@ -0,0 +1,113 @@ +from __future__ import unicode_literals +import logging +import threading +import zmq + +from robot_interface.endpoints.socket_base import SocketBase +from robot_interface.state import state +from robot_interface.core.config import settings + + +class FaceDetectionSender(SocketBase): + """ + Face detection sender endpoint. + + Subscribes to ALFaceDetection and forwards face detection + events over ZeroMQ. + """ + + def __init__(self, zmq_context, port=settings.agent_settings.face_sender_port): + super(FaceDetectionSender, self).__init__("face") + self.create_socket(zmq_context, zmq.PUB, port) + + self._face_service = None + self._memory_service = None + self._subscriber = None + self._thread = None + + def start_face_detection(self): + """ + Initializes ALFaceDetection and starts listening for face events. + """ + if not state.qi_session: + logging.info("No Qi session available. Not starting face detection.") + return + + import qi # Lazy import (same pattern as rest of codebase) + + self._face_service = state.qi_session.service("ALFaceDetection") + self._memory_service = state.qi_session.service("ALMemory") + + # Enable face detection + self._face_service.setTrackingEnabled(True) + self._face_service.setRecognitionEnabled(False) + + # Subscribe to FaceDetected memory event + self._subscriber = self._memory_service.subscriber("FaceDetected") + self._subscriber.signal.connect(self._on_face_detected) + + # Start keep-alive thread + self._thread = threading.Thread(target=self._face_loop) + self._thread.daemon = True + self._thread.start() + + logging.info("Face detection started.") + + def _face_loop(self): + """ + Keeps the face detection alive until shutdown. + """ + while not state.exit_event.is_set(): + state.exit_event.wait(0.1) + + self._cleanup() + + def _on_face_detected(self, value): + """ + Callback for ALMemory FaceDetected event. + + :param value: Face detection data structure from NAOqi + """ + if not value or len(value) < 2: + return + + timestamp = value[0] + faces = value[1] + + face_data = { + "timestamp": timestamp, + "face_count": len(faces), + "faces": [] + } + + for face in faces: + face_info = face[0] # Shape info + extra_info = face[1] # Extra info (ID, score, etc.) + + face_data["faces"].append({ + "alpha": face_info[1], + "beta": face_info[2], + "width": face_info[3], + "height": face_info[4], + "confidence": extra_info[1] if len(extra_info) > 1 else None + }) + + try: + self.socket.send_json(face_data) + except Exception: + logging.warn("Failed to send face detection data.") + + def _cleanup(self): + """ + Cleanup subscriptions and disable face detection. + """ + try: + if self._subscriber: + self._subscriber.signal.disconnect(self._on_face_detected) + + if self._face_service: + self._face_service.setTrackingEnabled(False) + + logging.info("Face detection stopped.") + except Exception: + logging.warn("Error during face detection cleanup.")