From 1b93fac09372fbe4ef834c7d6eaa8c63bb958130 Mon Sep 17 00:00:00 2001 From: Damien Mouratille Date: Mon, 8 Apr 2024 17:44:07 +0200 Subject: Pupil Invisible live-streaming connector --- src/argaze/utils/contexts/PupilLabs.py | 144 +++++++++++++++++++++ .../demo/pupillabs_live_stream_context_setup.json | 10 ++ 2 files changed, 154 insertions(+) create mode 100644 src/argaze/utils/contexts/PupilLabs.py create mode 100644 src/argaze/utils/demo/pupillabs_live_stream_context_setup.json diff --git a/src/argaze/utils/contexts/PupilLabs.py b/src/argaze/utils/contexts/PupilLabs.py new file mode 100644 index 0000000..9265f2c --- /dev/null +++ b/src/argaze/utils/contexts/PupilLabs.py @@ -0,0 +1,144 @@ +"""Handle network connection to Pupil Labs devices. Tested with Pupil Invisible. + Based on Pupil Labs' Realtime Python API. + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Damien Mouratille" +__credits__ = [] +__copyright__ = "Copyright 2024, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import sys +import logging +import time +import threading +from dataclasses import dataclass + +from argaze import ArFeatures, DataFeatures, GazeFeatures +from argaze.utils import UtilsFeatures + +import numpy +import cv2 + +from pupil_labs.realtime_api.simple import discover_one_device + +class LiveStream(ArFeatures.ArContext): + + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + + # Init ArContext class + super().__init__() + + def __enter__(self): + + logging.info('Pupil-Labs Device connexion starts...') + + # Create stop event + self.__stop_event = threading.Event() + + # Init timestamp + self.__start_time = time.time() + + # Look for devices. Returns as soon as it has found the first device. + self.__device = discover_one_device(max_search_duration_seconds=10) + + if self.__device is None: + logging.info('No device found. Exit!') + raise SystemExit(-1) + else: + logging.info('Device found. Stream loading.') + + # Open gaze stream + self.__gaze_thread = threading.Thread(target = self.__stream_gaze) + + logging.debug('> starting gaze thread...') + + self.__gaze_thread.start() + + # Open video stream + self.__video_thread = threading.Thread(target = self.__stream_video) + + logging.debug('> starting video thread...') + + self.__video_thread.start() + + + + return self + + + def __stream_gaze(self): + """Stream gaze.""" + + logging.debug('Stream gaze from Pupil Device') + + while not self.__stop_event.is_set(): + + try: + while True: + gaze = self.__device.receive_gaze_datum() + + gaze_timestamp = int((gaze.timestamp_unix_seconds - self.__start_time) * 1e3) + + logging.debug('Gaze received at %i timestamp', gaze_timestamp) + + # When gaze position is valid + if gaze.worn is True: + + self._process_gaze_position( + timestamp = gaze_timestamp, + x = int(gaze.x), + y = int(gaze.y)) + else: + # Process empty gaze position + logging.debug('Not worn at %i timestamp', gaze_timestamp) + + self._process_gaze_position(timestamp = gaze_timestamp) + + except KeyboardInterrupt: + pass + + + def __stream_video(self): + """Stream video.""" + + logging.debug('Stream video from Pupil Device') + + while not self.__stop_event.is_set(): + + try: + while True: + scene_frame, frame_datetime = self.__device.receive_scene_video_frame() + + scene_timestamp = int((frame_datetime - self.__start_time) * 1e3) + + logging.debug('Video received at %i timestamp', scene_timestamp) + + self._process_camera_image( + timestamp = scene_timestamp, + image = scene_frame) + + except KeyboardInterrupt: + pass + + @DataFeatures.PipelineStepExit + def __exit__(self, exception_type, exception_value, exception_traceback): + + logging.debug('Pupil-Labs context stops...') + + # Close data stream + self.__stop_event.set() + + # Stop streaming + threading.Thread.join(self.__gaze_thread) + threading.Thread.join(self.__video_thread) \ No newline at end of file diff --git a/src/argaze/utils/demo/pupillabs_live_stream_context_setup.json b/src/argaze/utils/demo/pupillabs_live_stream_context_setup.json new file mode 100644 index 0000000..3837a19 --- /dev/null +++ b/src/argaze/utils/demo/pupillabs_live_stream_context_setup.json @@ -0,0 +1,10 @@ +{ + "argaze.utils.contexts.PupilLabs.LiveStream" : { + "name": "PupilLabs", + "pipeline": "aruco_markers_pipeline.json", + "image_parameters": { + "draw_times": true, + "draw_exceptions": true + } + } +} \ No newline at end of file -- cgit v1.1