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