From 4325928bddea273592c5e315721c1cd179746e31 Mon Sep 17 00:00:00 2001 From: Damien Mouratille Date: Wed, 7 Aug 2024 16:37:46 +0200 Subject: Add Tobii Pro G3 + edit name contexts --- src/argaze/utils/contexts/PupilLabs.py | 139 -------------------- src/argaze/utils/contexts/PupilLabsInvisible.py | 140 +++++++++++++++++++++ src/argaze/utils/contexts/TobiiProGlasses3.py | 128 +++++++++++++++++++ src/argaze/utils/demo/aruco_markers_pipeline.json | 24 +--- src/argaze/utils/demo/gaze_analysis_pipeline.json | 2 +- .../pupillabs_invisible_live_stream_context.json | 6 + .../utils/demo/pupillabs_live_stream_context.json | 6 - .../utils/demo/tobii_g2_live_stream_context.json | 18 +++ .../utils/demo/tobii_g3_live_stream_context.json | 6 + .../utils/demo/tobii_live_stream_context.json | 18 --- 10 files changed, 302 insertions(+), 185 deletions(-) delete mode 100644 src/argaze/utils/contexts/PupilLabs.py create mode 100644 src/argaze/utils/contexts/PupilLabsInvisible.py create mode 100644 src/argaze/utils/contexts/TobiiProGlasses3.py create mode 100644 src/argaze/utils/demo/pupillabs_invisible_live_stream_context.json delete mode 100644 src/argaze/utils/demo/pupillabs_live_stream_context.json create mode 100644 src/argaze/utils/demo/tobii_g2_live_stream_context.json create mode 100644 src/argaze/utils/demo/tobii_g3_live_stream_context.json delete mode 100644 src/argaze/utils/demo/tobii_live_stream_context.json diff --git a/src/argaze/utils/contexts/PupilLabs.py b/src/argaze/utils/contexts/PupilLabs.py deleted file mode 100644 index 1bfb658..0000000 --- a/src/argaze/utils/contexts/PupilLabs.py +++ /dev/null @@ -1,139 +0,0 @@ -"""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.DataCaptureContext): - - @DataFeatures.PipelineStepInit - def __init__(self, **kwargs): - - # Init DataCaptureContext class - super().__init__() - - def __enter__(self): - - logging.info('Pupil-Labs Device connexion starts...') - - # 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 self.is_running(): - - 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 self.is_running(): - - 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() - - # Stop streaming - threading.Thread.join(self.__gaze_thread) - threading.Thread.join(self.__video_thread) diff --git a/src/argaze/utils/contexts/PupilLabsInvisible.py b/src/argaze/utils/contexts/PupilLabsInvisible.py new file mode 100644 index 0000000..5c9a138 --- /dev/null +++ b/src/argaze/utils/contexts/PupilLabsInvisible.py @@ -0,0 +1,140 @@ +"""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.DataCaptureContext): + + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + + # Init DataCaptureContext class + super().__init__() + + def __enter__(self): + + logging.info('Pupil-Labs Invisible connexion starts...') + + # 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 Invisible') + + while self.is_running(): + + 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 Invisible') + + while self.is_running(): + + 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() + + # Stop streaming + threading.Thread.join(self.__gaze_thread) + threading.Thread.join(self.__video_thread) diff --git a/src/argaze/utils/contexts/TobiiProGlasses3.py b/src/argaze/utils/contexts/TobiiProGlasses3.py new file mode 100644 index 0000000..a53c095 --- /dev/null +++ b/src/argaze/utils/contexts/TobiiProGlasses3.py @@ -0,0 +1,128 @@ +"""Handle network connection to Tobii Pro G3 devices. + Based on Tobii Realtime Python API. + g3pylib must be installed. +""" + +""" +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 dill +import threading +from dataclasses import dataclass +import numpy +import cv2 +import asyncio +import os + +from argaze import ArFeatures, DataFeatures, GazeFeatures +from argaze.utils import UtilsFeatures + + +from g3pylib import connect_to_glasses + + +class LiveStream(ArFeatures.DataCaptureContext): + + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + + # Init DataCaptureContext class + super().__init__() + + def __enter__(self): + + logging.info('Tobii Pro G3 connexion starts...') + + # Init timestamp + self.__start_time = time.time() + + self.__loop = asyncio.new_event_loop() + self.__loop.run_until_complete(self.__stream_rtsp()) + + return self + + async def __stream_rtsp(self): + """Stream video and gaze.""" + + logging.info('Stream gaze from Tobii Pro G3') + + while self.is_running(): + + try: + async with connect_to_glasses.with_zeroconf(True,10000) as g3: + async with g3.stream_rtsp(scene_camera=True, gaze=True) as streams: + async with streams.gaze.decode() as gaze_stream, streams.scene_camera.decode() as scene_stream: + while True: + frame, frame_timestamp = await scene_stream.get() + gaze, gaze_timestamp = await gaze_stream.get() + while gaze_timestamp is None or frame_timestamp is None: + if frame_timestamp is None: + frame, frame_timestamp = await scene_stream.get() + if gaze_timestamp is None: + gaze, gaze_timestamp = await gaze_stream.get() + while gaze_timestamp < frame_timestamp: + gaze, gaze_timestamp = await gaze_stream.get() + while gaze_timestamp is None: + gaze, gaze_timestamp = await gaze_stream.get() + + scene_frame = frame.to_ndarray(format="bgr24") + + gaze_timestamp = int((gaze_timestamp - self.__start_time) * 1e3) + + logging.debug('Gaze received at %i timestamp', gaze_timestamp) + + # If given gaze data + if "gaze2d" in gaze: + gaze2d = gaze["gaze2d"] + # Convert rational (x,y) to pixel location (x,y) + h, w = scene_frame.shape[:2] + gaze_scene = (int(gaze2d[0] * w), int(gaze2d[1] * h)) + + + self._process_gaze_position( + timestamp=gaze_timestamp, + x=gaze_scene[0], + y=gaze_scene[1]) + else: + # Process empty gaze position + logging.debug('Not worn at %i timestamp', gaze_timestamp) + + scene_timestamp = int((frame_timestamp - 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('Tobii Pro G3 context stops...') + + # Close data stream + self.stop() + diff --git a/src/argaze/utils/demo/aruco_markers_pipeline.json b/src/argaze/utils/demo/aruco_markers_pipeline.json index 0681bc3..8221cec 100644 --- a/src/argaze/utils/demo/aruco_markers_pipeline.json +++ b/src/argaze/utils/demo/aruco_markers_pipeline.json @@ -1,7 +1,7 @@ { "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "Head-mounted camera", - "size": [1920, 1080], + "size": [1088, 1080], "copy_background_into_scenes_frames": true, "aruco_detector": { "dictionary": "DICT_APRILTAG_16h5", @@ -56,7 +56,7 @@ }, "frames": { "GrayRectangle": { - "size": [1920, 1149], + "size": [1088, 1080], "background": "frame_background.jpg", "gaze_movement_identifier": { "argaze.GazeAnalysis.DispersionThresholdIdentification.GazeMovementIdentifier": { @@ -71,17 +71,12 @@ "argaze.GazeAnalysis.Basic.ScanPathAnalyzer": {}, "argaze.GazeAnalysis.KCoefficient.ScanPathAnalyzer": {}, "argaze.GazeAnalysis.NearestNeighborIndex.ScanPathAnalyzer": { - "size": [1920, 1149] + "size": [1088, 1080] }, "argaze.GazeAnalysis.ExploreExploitRatio.ScanPathAnalyzer": { "short_fixation_duration_threshold": 0 } }, - "observers": { - "recorders.ScanPathAnalysisRecorder": { - "path": "_export/records/scan_path_metrics.csv" - } - }, "layers": { "demo_layer": { "aoi_scene": "aoi_2d_scene.json", @@ -101,11 +96,6 @@ "n_max": 3 }, "argaze.GazeAnalysis.Entropy.AOIScanPathAnalyzer":{} - }, - "observers": { - "recorders.AOIScanPathAnalysisRecorder": { - "path": "_export/records/aoi_scan_path_metrics.csv" - } } } }, @@ -152,14 +142,6 @@ } } } - }, - "observers": { - "argaze.utils.UtilsFeatures.LookPerformanceRecorder": { - "path": "_export/records/look_performance.csv" - }, - "argaze.utils.UtilsFeatures.WatchPerformanceRecorder": { - "path": "_export/records/watch_performance.csv" - } } } } \ No newline at end of file diff --git a/src/argaze/utils/demo/gaze_analysis_pipeline.json b/src/argaze/utils/demo/gaze_analysis_pipeline.json index 8b8212e..6e23321 100644 --- a/src/argaze/utils/demo/gaze_analysis_pipeline.json +++ b/src/argaze/utils/demo/gaze_analysis_pipeline.json @@ -1,7 +1,7 @@ { "argaze.ArFeatures.ArFrame": { "name": "GrayRectangle", - "size": [1920, 1149], + "size": [1088, 1080], "background": "frame_background.jpg", "gaze_movement_identifier": { "argaze.GazeAnalysis.DispersionThresholdIdentification.GazeMovementIdentifier": { diff --git a/src/argaze/utils/demo/pupillabs_invisible_live_stream_context.json b/src/argaze/utils/demo/pupillabs_invisible_live_stream_context.json new file mode 100644 index 0000000..3418de6 --- /dev/null +++ b/src/argaze/utils/demo/pupillabs_invisible_live_stream_context.json @@ -0,0 +1,6 @@ +{ + "argaze.utils.contexts.PupilLabsInvisible.LiveStream" : { + "name": "PupilLabs Invisible", + "pipeline": "aruco_markers_pipeline.json" + } +} \ No newline at end of file diff --git a/src/argaze/utils/demo/pupillabs_live_stream_context.json b/src/argaze/utils/demo/pupillabs_live_stream_context.json deleted file mode 100644 index bcb7263..0000000 --- a/src/argaze/utils/demo/pupillabs_live_stream_context.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "argaze.utils.contexts.PupilLabs.LiveStream" : { - "name": "PupilLabs", - "pipeline": "aruco_markers_pipeline.json" - } -} \ No newline at end of file diff --git a/src/argaze/utils/demo/tobii_g2_live_stream_context.json b/src/argaze/utils/demo/tobii_g2_live_stream_context.json new file mode 100644 index 0000000..6950617 --- /dev/null +++ b/src/argaze/utils/demo/tobii_g2_live_stream_context.json @@ -0,0 +1,18 @@ +{ + "argaze.utils.contexts.TobiiProGlasses2.LiveStream" : { + "name": "Tobii Pro Glasses 2 live stream", + "address": "10.34.0.17", + "project": "MyProject", + "participant": "NewParticipant", + "configuration": { + "sys_ec_preset": "Indoor", + "sys_sc_width": 1920, + "sys_sc_height": 1080, + "sys_sc_fps": 25, + "sys_sc_preset": "Auto", + "sys_et_freq": 50, + "sys_mems_freq": 100 + }, + "pipeline": "aruco_markers_pipeline.json" + } +} \ No newline at end of file diff --git a/src/argaze/utils/demo/tobii_g3_live_stream_context.json b/src/argaze/utils/demo/tobii_g3_live_stream_context.json new file mode 100644 index 0000000..20f6ab1 --- /dev/null +++ b/src/argaze/utils/demo/tobii_g3_live_stream_context.json @@ -0,0 +1,6 @@ +{ + "argaze.utils.contexts.TobiiProGlasses3.LiveStream" : { + "name": "Tobii Pro Glasses 3 live stream", + "pipeline": "aruco_markers_pipeline.json" + } +} \ No newline at end of file diff --git a/src/argaze/utils/demo/tobii_live_stream_context.json b/src/argaze/utils/demo/tobii_live_stream_context.json deleted file mode 100644 index 6950617..0000000 --- a/src/argaze/utils/demo/tobii_live_stream_context.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "argaze.utils.contexts.TobiiProGlasses2.LiveStream" : { - "name": "Tobii Pro Glasses 2 live stream", - "address": "10.34.0.17", - "project": "MyProject", - "participant": "NewParticipant", - "configuration": { - "sys_ec_preset": "Indoor", - "sys_sc_width": 1920, - "sys_sc_height": 1080, - "sys_sc_fps": 25, - "sys_sc_preset": "Auto", - "sys_et_freq": 50, - "sys_mems_freq": 100 - }, - "pipeline": "aruco_markers_pipeline.json" - } -} \ No newline at end of file -- cgit v1.1 From e7cace52a6c1e0af88b715fce99c46fd48aa8bcc Mon Sep 17 00:00:00 2001 From: Damien Mouratille Date: Fri, 23 Aug 2024 15:35:37 +0200 Subject: Add Pupil Labs Neon device --- .../context_modules/pupil_labs.md | 32 ----- .../context_modules/pupil_labs_invisible.md | 32 +++++ .../context_modules/pupil_labs_neon.md | 32 +++++ .../context_modules/tobii_pro_glasses_3.md | 32 +++++ mkdocs.yml | 4 +- src/argaze/utils/contexts/PupilLabsNeon.py | 140 +++++++++++++++++++++ src/argaze/utils/demo/aruco_markers_pipeline.json | 6 +- src/argaze/utils/demo/gaze_analysis_pipeline.json | 6 +- .../demo/pupillabs_neon_live_stream_context.json | 6 + 9 files changed, 251 insertions(+), 39 deletions(-) delete mode 100644 docs/user_guide/eye_tracking_context/context_modules/pupil_labs.md create mode 100644 docs/user_guide/eye_tracking_context/context_modules/pupil_labs_invisible.md create mode 100644 docs/user_guide/eye_tracking_context/context_modules/pupil_labs_neon.md create mode 100644 docs/user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_3.md create mode 100644 src/argaze/utils/contexts/PupilLabsNeon.py create mode 100644 src/argaze/utils/demo/pupillabs_neon_live_stream_context.json diff --git a/docs/user_guide/eye_tracking_context/context_modules/pupil_labs.md b/docs/user_guide/eye_tracking_context/context_modules/pupil_labs.md deleted file mode 100644 index d2ec336..0000000 --- a/docs/user_guide/eye_tracking_context/context_modules/pupil_labs.md +++ /dev/null @@ -1,32 +0,0 @@ -Pupil Labs -========== - -ArGaze provides a ready-made context to work with Pupil Labs devices. - -To select a desired context, the JSON samples have to be edited and saved inside an [ArContext configuration](../configuration_and_execution.md) file. -Notice that the *pipeline* entry is mandatory. - -```json -{ - JSON sample - "pipeline": ... -} -``` - -Read more about [ArContext base class in code reference](../../../argaze.md/#argaze.ArFeatures.ArContext). - -## Live Stream - -::: argaze.utils.contexts.PupilLabs.LiveStream - -### JSON sample - -```json -{ - "argaze.utils.contexts.PupilLabs.LiveStream": { - "name": "Pupil Labs live stream", - "project": "my_experiment", - "pipeline": ... - } -} -``` diff --git a/docs/user_guide/eye_tracking_context/context_modules/pupil_labs_invisible.md b/docs/user_guide/eye_tracking_context/context_modules/pupil_labs_invisible.md new file mode 100644 index 0000000..1f4a94f --- /dev/null +++ b/docs/user_guide/eye_tracking_context/context_modules/pupil_labs_invisible.md @@ -0,0 +1,32 @@ +Pupil Labs Invisible +========== + +ArGaze provides a ready-made context to work with Pupil Labs Invisible device. + +To select a desired context, the JSON samples have to be edited and saved inside an [ArContext configuration](../configuration_and_execution.md) file. +Notice that the *pipeline* entry is mandatory. + +```json +{ + JSON sample + "pipeline": ... +} +``` + +Read more about [ArContext base class in code reference](../../../argaze.md/#argaze.ArFeatures.ArContext). + +## Live Stream + +::: argaze.utils.contexts.PupilLabsInvisible.LiveStream + +### JSON sample + +```json +{ + "argaze.utils.contexts.PupilLabsInvisible.LiveStream": { + "name": "Pupil Labs Invisible live stream", + "project": "my_experiment", + "pipeline": ... + } +} +``` diff --git a/docs/user_guide/eye_tracking_context/context_modules/pupil_labs_neon.md b/docs/user_guide/eye_tracking_context/context_modules/pupil_labs_neon.md new file mode 100644 index 0000000..535f5d5 --- /dev/null +++ b/docs/user_guide/eye_tracking_context/context_modules/pupil_labs_neon.md @@ -0,0 +1,32 @@ +Pupil Labs Neon +========== + +ArGaze provides a ready-made context to work with Pupil Labs Neon device. + +To select a desired context, the JSON samples have to be edited and saved inside an [ArContext configuration](../configuration_and_execution.md) file. +Notice that the *pipeline* entry is mandatory. + +```json +{ + JSON sample + "pipeline": ... +} +``` + +Read more about [ArContext base class in code reference](../../../argaze.md/#argaze.ArFeatures.ArContext). + +## Live Stream + +::: argaze.utils.contexts.PupilLabsNeon.LiveStream + +### JSON sample + +```json +{ + "argaze.utils.contexts.PupilLabsNeon.LiveStream": { + "name": "Pupil Labs Neon live stream", + "project": "my_experiment", + "pipeline": ... + } +} +``` diff --git a/docs/user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_3.md b/docs/user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_3.md new file mode 100644 index 0000000..3d37fcc --- /dev/null +++ b/docs/user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_3.md @@ -0,0 +1,32 @@ +Tobii Pro Glasses 3 +=================== + +ArGaze provides a ready-made context to work with Tobii Pro Glasses 3 devices. + +To select a desired context, the JSON samples have to be edited and saved inside an [ArContext configuration](../configuration_and_execution.md) file. +Notice that the *pipeline* entry is mandatory. + +```json +{ + JSON sample + "pipeline": ... +} +``` + +Read more about [ArContext base class in code reference](../../../argaze.md/#argaze.ArFeatures.ArContext). + +## Live Stream + +::: argaze.utils.contexts.TobiiProGlasses3.LiveStream + +### JSON sample + +```json +{ + "argaze.utils.contexts.TobiiProGlasses3.LiveStream": { + "name": "Tobii Pro Glasses 3 live stream", + "pipeline": ... + } +} +``` + diff --git a/mkdocs.yml b/mkdocs.yml index 8aadb7d..c35df15 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,7 +9,9 @@ nav: - user_guide/eye_tracking_context/configuration_and_execution.md - Context Modules: - user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_2.md - - user_guide/eye_tracking_context/context_modules/pupil_labs.md + - user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_3.md + - user_guide/eye_tracking_context/context_modules/pupil_lab_invisible.md + - user_guide/eye_tracking_context/context_modules/pupil_lab_neon.md - user_guide/eye_tracking_context/context_modules/opencv.md - user_guide/eye_tracking_context/context_modules/random.md - Advanced Topics: diff --git a/src/argaze/utils/contexts/PupilLabsNeon.py b/src/argaze/utils/contexts/PupilLabsNeon.py new file mode 100644 index 0000000..e7d1f47 --- /dev/null +++ b/src/argaze/utils/contexts/PupilLabsNeon.py @@ -0,0 +1,140 @@ +"""Handle network connection to Pupil Labs devices. Tested with Pupil Neon. + 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.DataCaptureContext): + + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + + # Init DataCaptureContext class + super().__init__() + + def __enter__(self): + + logging.info('Pupil-Labs Neon connexion starts...') + + # 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 Neon') + + while self.is_running(): + + 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 Neon') + + while self.is_running(): + + 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() + + # Stop streaming + threading.Thread.join(self.__gaze_thread) + threading.Thread.join(self.__video_thread) diff --git a/src/argaze/utils/demo/aruco_markers_pipeline.json b/src/argaze/utils/demo/aruco_markers_pipeline.json index 8221cec..9dc8327 100644 --- a/src/argaze/utils/demo/aruco_markers_pipeline.json +++ b/src/argaze/utils/demo/aruco_markers_pipeline.json @@ -1,7 +1,7 @@ { "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "Head-mounted camera", - "size": [1088, 1080], + "size": [1600, 1200], "copy_background_into_scenes_frames": true, "aruco_detector": { "dictionary": "DICT_APRILTAG_16h5", @@ -56,7 +56,7 @@ }, "frames": { "GrayRectangle": { - "size": [1088, 1080], + "size": [1600, 1200], "background": "frame_background.jpg", "gaze_movement_identifier": { "argaze.GazeAnalysis.DispersionThresholdIdentification.GazeMovementIdentifier": { @@ -71,7 +71,7 @@ "argaze.GazeAnalysis.Basic.ScanPathAnalyzer": {}, "argaze.GazeAnalysis.KCoefficient.ScanPathAnalyzer": {}, "argaze.GazeAnalysis.NearestNeighborIndex.ScanPathAnalyzer": { - "size": [1088, 1080] + "size": [1600, 1200] }, "argaze.GazeAnalysis.ExploreExploitRatio.ScanPathAnalyzer": { "short_fixation_duration_threshold": 0 diff --git a/src/argaze/utils/demo/gaze_analysis_pipeline.json b/src/argaze/utils/demo/gaze_analysis_pipeline.json index 6e23321..cc182ce 100644 --- a/src/argaze/utils/demo/gaze_analysis_pipeline.json +++ b/src/argaze/utils/demo/gaze_analysis_pipeline.json @@ -1,7 +1,7 @@ { "argaze.ArFeatures.ArFrame": { "name": "GrayRectangle", - "size": [1088, 1080], + "size": [1600, 1200], "background": "frame_background.jpg", "gaze_movement_identifier": { "argaze.GazeAnalysis.DispersionThresholdIdentification.GazeMovementIdentifier": { @@ -126,8 +126,8 @@ }, "recorders.FrameImageRecorder": { "path": "_export/records/video.mp4", - "width": 1920, - "height": 1080, + "width": 1600, + "height": 1200, "fps": 15 } } diff --git a/src/argaze/utils/demo/pupillabs_neon_live_stream_context.json b/src/argaze/utils/demo/pupillabs_neon_live_stream_context.json new file mode 100644 index 0000000..a87c30e --- /dev/null +++ b/src/argaze/utils/demo/pupillabs_neon_live_stream_context.json @@ -0,0 +1,6 @@ +{ + "argaze.utils.contexts.PupilLabsNeon.LiveStream" : { + "name": "PupilLabs Neon", + "pipeline": "aruco_markers_pipeline.json" + } +} \ No newline at end of file -- cgit v1.1 From 84fd53edaa66cda11e4e47abce3fbe7cc7ace657 Mon Sep 17 00:00:00 2001 From: Damien Mouratille Date: Fri, 23 Aug 2024 17:04:14 +0200 Subject: update docs --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index c35df15..6384ae0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,8 +10,8 @@ nav: - Context Modules: - user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_2.md - user_guide/eye_tracking_context/context_modules/tobii_pro_glasses_3.md - - user_guide/eye_tracking_context/context_modules/pupil_lab_invisible.md - - user_guide/eye_tracking_context/context_modules/pupil_lab_neon.md + - user_guide/eye_tracking_context/context_modules/pupil_labs_invisible.md + - user_guide/eye_tracking_context/context_modules/pupil_labs_neon.md - user_guide/eye_tracking_context/context_modules/opencv.md - user_guide/eye_tracking_context/context_modules/random.md - Advanced Topics: -- cgit v1.1