From 90dea0c2db811802830b6ca5a3b63f4f59c0b57b Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 30 Mar 2021 09:58:11 +0200 Subject: [PATCH 01/65] Change default annotation hotkey from "E" to "a" --- pupil_src/shared_modules/annotations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/annotations.py b/pupil_src/shared_modules/annotations.py index b453218f68..80c38c6db7 100644 --- a/pupil_src/shared_modules/annotations.py +++ b/pupil_src/shared_modules/annotations.py @@ -57,18 +57,20 @@ class AnnotationPlugin(Plugin, abc.ABC): icon_chr = chr(0xE866) icon_font = "pupil_icons" + _DEFAULT_HOTKEY = "a" + def __init__(self, g_pool, annotation_definitions=None): super().__init__(g_pool) self.menu = None self._annotation_list_menu = None if annotation_definitions is None: - annotation_definitions = [["My annotation", "E"]] + annotation_definitions = [["My annotation", self._DEFAULT_HOTKEY]] self._initial_annotation_definitions = annotation_definitions self._definition_to_buttons = {} self._new_annotation_label = "new annotation label" - self._new_annotation_hotkey = "E" + self._new_annotation_hotkey = self._DEFAULT_HOTKEY def get_init_dict(self): annotation_definitions = list(self._definition_to_buttons.keys()) From c69da7ad1461fb4f27355be7d88e1409f4665a96 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Wed, 31 Mar 2021 11:46:12 +0200 Subject: [PATCH 02/65] Pupil Groups: Sort imports --- pupil_src/shared_modules/pupil_groups.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pupil_src/shared_modules/pupil_groups.py b/pupil_src/shared_modules/pupil_groups.py index 10315d2467..0c882be936 100644 --- a/pupil_src/shared_modules/pupil_groups.py +++ b/pupil_src/shared_modules/pupil_groups.py @@ -9,15 +9,18 @@ ---------------------------------------------------------------------------~(*) """ -import zmq, time, uuid -from pyre import Pyre, PyreEvent, zhelper -from pyglui import ui -from plugin import Plugin -from zmq_tools import Msg_Dispatcher, Msg_Receiver +import logging +import time +import uuid + import msgpack as serializer +import zmq +from pyglui import ui +from pyre import Pyre, PyreEvent, zhelper -import logging import os_utils +from plugin import Plugin +from zmq_tools import Msg_Dispatcher, Msg_Receiver os_utils.patch_pyre_zhelper_cdll() logger = logging.getLogger(__name__) From 3c1800283df2cf11a00d1a8d747e5e8e195bb226 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Wed, 31 Mar 2021 11:48:13 +0200 Subject: [PATCH 03/65] Log full exception on debug level --- pupil_src/shared_modules/pupil_groups.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pupil_src/shared_modules/pupil_groups.py b/pupil_src/shared_modules/pupil_groups.py index 0c882be936..20fcc2fcfc 100644 --- a/pupil_src/shared_modules/pupil_groups.py +++ b/pupil_src/shared_modules/pupil_groups.py @@ -11,6 +11,7 @@ import logging import time +import traceback import uuid import msgpack as serializer @@ -264,6 +265,8 @@ def shutdown_group_member(node): event.peer_name, event.peer_uuid ) ) + logger.debug(traceback.format_exc()) + elif event.type == "JOIN" and event.group == self.active_group: local_out.notify( { From ba564ef6928802051c2e8a2420e67a0027f09d0d Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Wed, 31 Mar 2021 12:15:06 +0200 Subject: [PATCH 04/65] Remove out-of-date function argument --- pupil_src/shared_modules/pupil_groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/pupil_groups.py b/pupil_src/shared_modules/pupil_groups.py index 20fcc2fcfc..1abdc1791e 100644 --- a/pupil_src/shared_modules/pupil_groups.py +++ b/pupil_src/shared_modules/pupil_groups.py @@ -248,7 +248,7 @@ def shutdown_group_member(node): for msg in event.msg: try: # try to unpack data - notification = serializer.loads(msg, encoding="utf-8") + notification = serializer.loads(msg) # test if dictionary and if `subject` key is present notification["subject"] # add peer information From c8ac7f1e1c085e0377e478a1d6a0208a7535126d Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Wed, 31 Mar 2021 12:35:10 +0200 Subject: [PATCH 05/65] Use msgpack explicitly --- pupil_src/shared_modules/pupil_groups.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pupil_src/shared_modules/pupil_groups.py b/pupil_src/shared_modules/pupil_groups.py index 1abdc1791e..53eccef8ca 100644 --- a/pupil_src/shared_modules/pupil_groups.py +++ b/pupil_src/shared_modules/pupil_groups.py @@ -14,7 +14,7 @@ import traceback import uuid -import msgpack as serializer +import msgpack import zmq from pyglui import ui from pyre import Pyre, PyreEvent, zhelper @@ -233,12 +233,12 @@ def shutdown_group_member(node): remote_key = "remote_notify" if notification[remote_key] == "all": del notification[remote_key] - serialized = serializer.dumps(notification) + serialized = msgpack.packb(notification) group_member.shout(self.active_group, serialized) else: peer_uuid_bytes = notification[remote_key] del notification[remote_key] - serialized = serializer.dumps(notification) + serialized = msgpack.packb(notification) peer_uuid = uuid.UUID(bytes=peer_uuid_bytes) group_member.whisper(peer_uuid, serialized) @@ -248,7 +248,7 @@ def shutdown_group_member(node): for msg in event.msg: try: # try to unpack data - notification = serializer.loads(msg) + notification = msgpack.unpackb(msg) # test if dictionary and if `subject` key is present notification["subject"] # add peer information From 141b98cf6896c33711f6b07324b4821240a255b2 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 31 Mar 2021 19:21:59 +0200 Subject: [PATCH 06/65] Add DisabledPupilProducer pupil producer stup This class is used as a substitute of an actual pupil producer implementation. It stubs the interface of a pupil producer so that all the plugins that depend on there being a producer still work. --- pupil_src/shared_modules/pupil_producers.py | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pupil_src/shared_modules/pupil_producers.py b/pupil_src/shared_modules/pupil_producers.py index afb7b83f42..8ff01e7fbf 100644 --- a/pupil_src/shared_modules/pupil_producers.py +++ b/pupil_src/shared_modules/pupil_producers.py @@ -291,6 +291,53 @@ def _legend_font(self, scale): self.glfont.pop_state() +class DisabledPupilProducer(Pupil_Producer_Base): + """ + This is a stub implementation of a pupil producer, + intended to be used when no (other) pupil producer is available. + """ + + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + if g_pool.app == "player": + recording = PupilRecording(rec_dir=g_pool.rec_dir) + meta_info = recording.meta_info + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE + ): + # Enable in Player only if Pupil Invisible recording + return True + return False + + @classmethod + def plugin_menu_label(cls) -> str: + raise RuntimeError() # This method should never be called + return "Disabled Pupil Producer" + + @classmethod + def pupil_data_source_selection_order(cls) -> float: + raise RuntimeError() # This method should never be called + return 0.1 + + def __init__(self, g_pool): + super().__init__(g_pool) + # Create empty pupil_positions for all plugins that depend on it + pupil_data = pm.PupilDataBisector(data=fm.PLData([], [], [])) + g_pool.pupil_positions = pupil_data + self._pupil_changed_announcer.announce_existing() + logger.debug("pupil positions changed") + + def init_ui(self): + pass + + def deinit_ui(self): + pass + + def _refresh_timelines(self): + pass + + class Pupil_From_Recording(Pupil_Producer_Base): @classmethod def is_available_within_context(cls, g_pool) -> bool: From 49f6f6860716a95e09d169790cf074ee3e0f71d6 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 31 Mar 2021 19:23:11 +0200 Subject: [PATCH 07/65] Add DisabledPupilProducer to player default plugin list --- pupil_src/launchables/player.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pupil_src/launchables/player.py b/pupil_src/launchables/player.py index 97069b43ec..64c6ad42e7 100644 --- a/pupil_src/launchables/player.py +++ b/pupil_src/launchables/player.py @@ -113,7 +113,11 @@ def player( from annotations import Annotation_Player from raw_data_exporter import Raw_Data_Exporter from log_history import Log_History - from pupil_producers import Pupil_From_Recording, Offline_Pupil_Detection + from pupil_producers import ( + DisabledPupilProducer, + Pupil_From_Recording, + Offline_Pupil_Detection, + ) from gaze_producer.gaze_from_recording import GazeFromRecording from gaze_producer.gaze_from_offline_calibration import ( GazeFromOfflineCalibration, @@ -180,6 +184,7 @@ def interrupt_handler(sig, frame): Raw_Data_Exporter, Annotation_Player, Log_History, + DisabledPupilProducer, Pupil_From_Recording, Offline_Pupil_Detection, GazeFromRecording, @@ -576,6 +581,7 @@ def set_window_size(): # In priority order (first is default) ("Pupil_From_Recording", {}), ("Offline_Pupil_Detection", {}), + ("DisabledPupilProducer", {}), ] _pupil_producer_plugins = list(reversed(_pupil_producer_plugins)) _gaze_producer_plugins = [ From 75e8db831eadefd2c01cf65e268786fdf553c883 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 31 Mar 2021 19:24:17 +0200 Subject: [PATCH 08/65] Extract code for determining available pupil producers into a separate method --- pupil_src/shared_modules/pupil_producers.py | 27 ++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pupil_src/shared_modules/pupil_producers.py b/pupil_src/shared_modules/pupil_producers.py index 8ff01e7fbf..6159280a79 100644 --- a/pupil_src/shared_modules/pupil_producers.py +++ b/pupil_src/shared_modules/pupil_producers.py @@ -57,6 +57,21 @@ def plugin_menu_label(cls) -> str: def pupil_data_source_selection_label(cls) -> str: return cls.plugin_menu_label() + @staticmethod + def available_pupil_producer_plugins(g_pool) -> list: + def is_plugin_included(p, g_pool) -> bool: + # Skip plugins that are not pupil producers + if not issubclass(p, Pupil_Producer_Base): + return False + # Skip pupil producers that are not available within g_pool context + if not p.is_available_within_context(g_pool): + return False + return True + + return [ + p for p in g_pool.plugin_by_name.values() if is_plugin_included(p, g_pool) + ] + @classmethod def pupil_data_source_selection_order(cls) -> float: return float("inf") @@ -76,17 +91,7 @@ def __init__(self, g_pool): def init_ui(self): self.add_menu() - pupil_producer_plugins = [ - p - for p in self.g_pool.plugin_by_name.values() - if issubclass(p, Pupil_Producer_Base) - ] - # Skip pupil producers that are not available within g_pool context - pupil_producer_plugins = [ - p - for p in pupil_producer_plugins - if p.is_available_within_context(self.g_pool) - ] + pupil_producer_plugins = self.available_pupil_producer_plugins(self.g_pool) pupil_producer_plugins.sort(key=lambda p: p.pupil_data_source_selection_label()) pupil_producer_plugins.sort(key=lambda p: p.pupil_data_source_selection_order()) pupil_producer_labels = [ From f36c8dbee4d59b91eedc901e289a43bfec04b353 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 31 Mar 2021 19:25:52 +0200 Subject: [PATCH 09/65] Disable Pupil_From_Recording producer for Invisible recordings --- pupil_src/shared_modules/pupil_producers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pupil_src/shared_modules/pupil_producers.py b/pupil_src/shared_modules/pupil_producers.py index 6159280a79..88987ea1d4 100644 --- a/pupil_src/shared_modules/pupil_producers.py +++ b/pupil_src/shared_modules/pupil_producers.py @@ -355,6 +355,12 @@ def is_available_within_context(cls, g_pool) -> bool: ): # Disable pupil from recording in Player if Pupil Mobile recording return False + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE + ): + # Disable pupil from recording in Player if Pupil Invisible recording + return False return super().is_available_within_context(g_pool) @classmethod From 84b8f028506fd95ddb2b36b2fb35d48050c771a5 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 31 Mar 2021 19:26:38 +0200 Subject: [PATCH 10/65] Exclude DisabledPupilProducer from pupil producer list Since this is a stub implementation, it is not considered a valid pupil producer --- pupil_src/shared_modules/pupil_producers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pupil_src/shared_modules/pupil_producers.py b/pupil_src/shared_modules/pupil_producers.py index 88987ea1d4..03bd755602 100644 --- a/pupil_src/shared_modules/pupil_producers.py +++ b/pupil_src/shared_modules/pupil_producers.py @@ -63,6 +63,9 @@ def is_plugin_included(p, g_pool) -> bool: # Skip plugins that are not pupil producers if not issubclass(p, Pupil_Producer_Base): return False + # Skip pupil producer stub + if p is DisabledPupilProducer: + return False # Skip pupil producers that are not available within g_pool context if not p.is_available_within_context(g_pool): return False From 3d3646bdd4acfac6a78da40fe2ee4782194fd23f Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 31 Mar 2021 19:27:40 +0200 Subject: [PATCH 11/65] Disable pupil positions export when no pupil producer is available --- pupil_src/shared_modules/raw_data_exporter.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pupil_src/shared_modules/raw_data_exporter.py b/pupil_src/shared_modules/raw_data_exporter.py index 66f1bbf174..3ff2aaa31d 100644 --- a/pupil_src/shared_modules/raw_data_exporter.py +++ b/pupil_src/shared_modules/raw_data_exporter.py @@ -21,6 +21,9 @@ import player_methods as pm from plugin import Plugin +from pupil_producers import Pupil_Producer_Base + + # logging logger = logging.getLogger(__name__) @@ -116,10 +119,20 @@ def __init__( should_export_gaze_positions=True, ): super().__init__(g_pool) + + # If no pupil producer is available, don't export pupil positions + if not self._is_pupil_producer_avaiable: + should_export_pupil_positions = False + self.should_export_pupil_positions = should_export_pupil_positions self.should_export_field_info = should_export_field_info self.should_export_gaze_positions = should_export_gaze_positions + @property + def _is_pupil_producer_avaiable(self) -> bool: + producers = Pupil_Producer_Base.available_pupil_producer_plugins(self.g_pool) + return len(producers) > 0 + def init_ui(self): self.add_menu() self.menu.label = "Raw Data Exporter" @@ -129,11 +142,13 @@ def init_ui(self): "Select your export frame range using the trim marks in the seek bar. This will affect all exporting plugins." ) ) - self.menu.append( - ui.Switch( - "should_export_pupil_positions", self, label="Export Pupil Positions" - ) + + pupil_positions_switch = ui.Switch( + "should_export_pupil_positions", self, label="Export Pupil Positions" ) + pupil_positions_switch.read_only = not self._is_pupil_producer_avaiable + self.menu.append(pupil_positions_switch) + self.menu.append( ui.Switch( "should_export_field_info", From 39c53faf1bccf87c501c556b7869ff8cf6cfbfc6 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Thu, 1 Apr 2021 14:21:43 +0200 Subject: [PATCH 12/65] Replace "set_pupil_detection_enabled" with "pupil_detector.set_enabled" notification subject --- pupil_src/launchables/world.py | 6 +++--- pupil_src/shared_modules/hololens_relay.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pupil_src/launchables/world.py b/pupil_src/launchables/world.py index 361de0e69b..dcf593fa0f 100644 --- a/pupil_src/launchables/world.py +++ b/pupil_src/launchables/world.py @@ -105,7 +105,7 @@ def detection_enabled_getter() -> bool: def detection_enabled_setter(is_on: bool): g_pool.pupil_detection_enabled = is_on - n = {"subject": "set_pupil_detection_enabled", "value": is_on} + n = {"subject": "pupil_detector.set_enabled", "value": is_on} ipc_pub.notify(n) try: @@ -478,7 +478,7 @@ def get_dt(): def handle_notifications(noti): subject = noti["subject"] - if subject == "set_pupil_detection_enabled": + if subject == "pupil_detector.set_enabled": g_pool.pupil_detection_enabled = noti["value"] elif subject == "start_plugin": try: @@ -494,7 +494,7 @@ def handle_notifications(noti): g_pool.plugins.clean() elif subject == "eye_process.started": noti = { - "subject": "set_pupil_detection_enabled", + "subject": "pupil_detector.set_enabled", "value": g_pool.pupil_detection_enabled, } ipc_pub.notify(noti) diff --git a/pupil_src/shared_modules/hololens_relay.py b/pupil_src/shared_modules/hololens_relay.py index 94613aa087..e5ca3af75e 100644 --- a/pupil_src/shared_modules/hololens_relay.py +++ b/pupil_src/shared_modules/hololens_relay.py @@ -393,7 +393,7 @@ def on_recv(self, socket, ipc_pub): calib_method = "HMD_Calibration_3D" ipc_pub.notify({"subject": "start_plugin", "name": calib_method}) - ipc_pub.notify({"subject": "set_pupil_detection_enabled", "value": True}) + ipc_pub.notify({"subject": "pupil_detector.set_enabled", "value": True}) ipc_pub.notify( { "subject": "eye_process.should_start.{}".format(0), From 73b5747a93903160392d84148a841bdc1bae6f7d Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Thu, 1 Apr 2021 14:22:56 +0200 Subject: [PATCH 13/65] Explicitly define the type for g_pool.pupil_detection_enabled and pupil_detection_enabled ui switch value --- pupil_src/launchables/world.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pupil_src/launchables/world.py b/pupil_src/launchables/world.py index dcf593fa0f..ff3f88cdf2 100644 --- a/pupil_src/launchables/world.py +++ b/pupil_src/launchables/world.py @@ -100,10 +100,11 @@ def start_stop_eye(eye_id, make_alive): else: stop_eye_process(eye_id) - def detection_enabled_getter() -> bool: - return g_pool.pupil_detection_enabled + def detection_enabled_getter() -> int: + return int(g_pool.pupil_detection_enabled) - def detection_enabled_setter(is_on: bool): + def detection_enabled_setter(value: int): + is_on = bool(value) g_pool.pupil_detection_enabled = is_on n = {"subject": "pupil_detector.set_enabled", "value": is_on} ipc_pub.notify(n) @@ -466,8 +467,8 @@ def get_dt(): g_pool.min_calibration_confidence = session_settings.get( "min_calibration_confidence", 0.8 ) - g_pool.pupil_detection_enabled = session_settings.get( - "pupil_detection_enabled", True + g_pool.pupil_detection_enabled = bool( + session_settings.get("pupil_detection_enabled", True) ) g_pool.active_gaze_mapping_plugin = None g_pool.capture = None From 85a388f839e6f7c5372f0c180faac26ab39e2e62 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Tue, 6 Apr 2021 11:02:33 +0200 Subject: [PATCH 14/65] Sort imports --- pupil_src/shared_modules/accuracy_visualizer.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index 4e5d7b1930..dc654d1e73 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -10,13 +10,13 @@ """ import logging -from collections import namedtuple import typing as T +from collections import namedtuple -import OpenGL.GL as gl import numpy as np +import OpenGL.GL as gl from pyglui import ui -from pyglui.cygl.utils import draw_points_norm, draw_polyline_norm, RGBA +from pyglui.cygl.utils import RGBA, draw_points_norm, draw_polyline_norm from scipy.spatial import ConvexHull from calibration_choreography import ( @@ -24,15 +24,13 @@ ChoreographyMode, ChoreographyNotification, ) -from plugin import Plugin - from gaze_mapping import gazer_classes_by_class_name, registered_gazer_classes from gaze_mapping.notifications import ( - CalibrationSetupNotification, CalibrationResultNotification, + CalibrationSetupNotification, ) from gaze_mapping.utils import closest_matches_monocular - +from plugin import Plugin logger = logging.getLogger(__name__) From 6fd9adcf0d2575bead159013ea1b004d704ee4f4 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Tue, 6 Apr 2021 11:02:46 +0200 Subject: [PATCH 15/65] Remove unused import --- pupil_src/shared_modules/accuracy_visualizer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index dc654d1e73..8367370762 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -11,7 +11,6 @@ import logging import typing as T -from collections import namedtuple import numpy as np import OpenGL.GL as gl From a5ad15ade40234420dcda60117c2d69b41f373dd Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Tue, 6 Apr 2021 11:09:47 +0200 Subject: [PATCH 16/65] Catch QHull errors Fixes #2122 --- pupil_src/shared_modules/accuracy_visualizer.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index 8367370762..92b3bf79c6 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -10,13 +10,14 @@ """ import logging +import traceback import typing as T import numpy as np import OpenGL.GL as gl +import scipy.spatial from pyglui import ui from pyglui.cygl.utils import RGBA, draw_points_norm, draw_polyline_norm -from scipy.spatial import ConvexHull from calibration_choreography import ( ChoreographyAction, @@ -397,8 +398,13 @@ def recalculate(self): self.error_lines = results.error_lines ref_locations = results.correlation.norm_space[1::2, :] if len(ref_locations) >= 3: - hull = ConvexHull(ref_locations) # requires at least 3 points - self.calibration_area = hull.points[hull.vertices, :] + try: + # requires at least 3 points + hull = scipy.spatial.ConvexHull(ref_locations) + self.calibration_area = hull.points[hull.vertices, :] + except scipy.spatial.qhull.QhullError: + logger.warning("Calibration area could not be calculated") + logger.debug(traceback.format_exc()) @staticmethod def calc_acc_prec_errlines( From f7b53efb28f652194b39a1e5b21ae8596d84e31d Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Thu, 8 Apr 2021 11:32:05 +0200 Subject: [PATCH 17/65] Extract hotkey constants into a separate Hotkey namespace --- pupil_src/launchables/player.py | 3 +- .../calibration_choreography/base_plugin.py | 5 +- .../camera_intrinsics_estimation.py | 8 ++- pupil_src/shared_modules/fixation_detector.py | 5 +- pupil_src/shared_modules/hotkey.py | 61 +++++++++++++++++++ pupil_src/shared_modules/recorder.py | 8 ++- .../surface_tracker/surface_tracker.py | 3 +- 7 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 pupil_src/shared_modules/hotkey.py diff --git a/pupil_src/launchables/player.py b/pupil_src/launchables/player.py index 64c6ad42e7..c6010a57c3 100644 --- a/pupil_src/launchables/player.py +++ b/pupil_src/launchables/player.py @@ -93,6 +93,7 @@ def player( import player_methods as pm from pupil_recording import PupilRecording from csv_utils import write_key_value_file + from hotkey import Hotkey # Plug-ins from plugin import Plugin, Plugin_List, import_runtime_plugins @@ -567,7 +568,7 @@ def set_window_size(): label=chr(0xE2C5), getter=lambda: False, setter=do_export, - hotkey="e", + hotkey=Hotkey.EXPORT_START_PLAYER_HOTKEY(), label_font="pupil_icons", ) g_pool.quickbar.extend([g_pool.export_button]) diff --git a/pupil_src/shared_modules/calibration_choreography/base_plugin.py b/pupil_src/shared_modules/calibration_choreography/base_plugin.py index c756a77eea..c4ff870972 100644 --- a/pupil_src/shared_modules/calibration_choreography/base_plugin.py +++ b/pupil_src/shared_modules/calibration_choreography/base_plugin.py @@ -17,6 +17,7 @@ import audio from pyglui import ui from plugin import Plugin +from hotkey import Hotkey from gaze_mapping.gazer_base import GazerBase from gaze_mapping import default_gazer_class @@ -422,7 +423,7 @@ def validation_setter(should_be_on): "is_active", self, label="C", - hotkey="c", + hotkey=Hotkey.GAZE_CALIBRATION_CAPTURE_HOTKEY(), setter=calibration_setter, on_color=self._THUMBNAIL_COLOR_ON, ) @@ -431,7 +432,7 @@ def validation_setter(should_be_on): "is_active", self, label="T", - hotkey="t", + hotkey=Hotkey.GAZE_VALIDATION_CAPTURE_HOTKEY(), setter=validation_setter, on_color=self._THUMBNAIL_COLOR_ON, ) diff --git a/pupil_src/shared_modules/camera_intrinsics_estimation.py b/pupil_src/shared_modules/camera_intrinsics_estimation.py index 0e688f1636..399c03adf6 100644 --- a/pupil_src/shared_modules/camera_intrinsics_estimation.py +++ b/pupil_src/shared_modules/camera_intrinsics_estimation.py @@ -34,6 +34,8 @@ from plugin import Plugin +from hotkey import Hotkey + # logging import logging @@ -153,7 +155,11 @@ def get_monitors_idx_list(): ) self.button = ui.Thumb( - "collect_new", self, setter=self.advance, label="I", hotkey="i" + "collect_new", + self, + setter=self.advance, + label="I", + hotkey=Hotkey.CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(), ) self.button.on_color[:] = (0.3, 0.2, 1.0, 0.9) self.g_pool.quickbar.insert(0, self.button) diff --git a/pupil_src/shared_modules/fixation_detector.py b/pupil_src/shared_modules/fixation_detector.py index 4853f89915..8dccf572b2 100644 --- a/pupil_src/shared_modules/fixation_detector.py +++ b/pupil_src/shared_modules/fixation_detector.py @@ -51,6 +51,7 @@ from methods import denormalize from plugin import Plugin from pupil_recording import PupilRecording, RecordingInfo +from hotkey import Hotkey logger = logging.getLogger(__name__) @@ -415,7 +416,7 @@ def jump_prev_fixation(_): setter=jump_next_fixation, getter=lambda: False, label=chr(0xE044), - hotkey="f", + hotkey=Hotkey.FIXATION_NEXT_PLAYER_HOTKEY(), label_font="pupil_icons", ) self.next_fix_button.status_text = "Next Fixation" @@ -426,7 +427,7 @@ def jump_prev_fixation(_): setter=jump_prev_fixation, getter=lambda: False, label=chr(0xE045), - hotkey="F", + hotkey=Hotkey.FIXATION_PREV_PLAYER_HOTKEY(), label_font="pupil_icons", ) self.prev_fix_button.status_text = "Previous Fixation" diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py new file mode 100644 index 0000000000..b3bf20c41a --- /dev/null +++ b/pupil_src/shared_modules/hotkey.py @@ -0,0 +1,61 @@ +""" +(*)~--------------------------------------------------------------------------- +Pupil - eye tracking platform +Copyright (C) 2012-2021 Pupil Labs + +Distributed under the terms of the GNU +Lesser General Public License (LGPL v3.0). +See COPYING and COPYING.LESSER for license details. +---------------------------------------------------------------------------~(*) +""" + + +class Hotkey: + """""" + + @staticmethod + def CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(): + return "i" + + @staticmethod + def EXPORT_START_PLAYER_HOTKEY(): + return "e" + + @staticmethod + def FIXATION_NEXT_PLAYER_HOTKEY(): + return "f" + + @staticmethod + def FIXATION_PREV_PLAYER_HOTKEY(): + return "F" + + @staticmethod + def GAZE_CALIBRATION_CAPTURE_HOTKEY(): + return "c" + + @staticmethod + def GAZE_VALIDATION_CAPTURE_HOTKEY(): + return "t" + + @staticmethod + def RECORDER_RUNNING_TOGGLE_CAPTURE_HOTKEY(): + return "r" + + @staticmethod + def SURFACE_TRACKER_ADD_SURFACE_CAPTURE_AND_PLAYER_HOTKEY(): + return "a" + + @staticmethod + def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): + # This is only implicitly used by pyglui.ui.Seek_Bar + return 263 + + @staticmethod + def SEEK_BAR_MOVE_FORWARDSS_PLAYER_HOTKEY(): + # This is only implicitly used by pyglui.ui.Seek_Bar + return 262 + + @staticmethod + def SEEK_BAR_PLAY_PAUSE_PLAYER_HOTKEY(): + # This is only implicitly used by pyglui.ui.Seek_Bar + return 32 diff --git a/pupil_src/shared_modules/recorder.py b/pupil_src/shared_modules/recorder.py index e1a131c5b8..698fc94f4b 100644 --- a/pupil_src/shared_modules/recorder.py +++ b/pupil_src/shared_modules/recorder.py @@ -38,6 +38,8 @@ # from scipy.interpolate import UnivariateSpline from plugin import System_Plugin_Base +from hotkey import Hotkey + logger = logging.getLogger(__name__) @@ -185,7 +187,11 @@ def init_ui(self): ) ) self.button = ui.Thumb( - "running", self, setter=self.toggle, label="R", hotkey="r" + "running", + self, + setter=self.toggle, + label="R", + hotkey=Hotkey.RECORDER_RUNNING_TOGGLE_CAPTURE_HOTKEY(), ) self.button.on_color[:] = (1, 0.0, 0.0, 0.8) self.g_pool.quickbar.insert(2, self.button) diff --git a/pupil_src/shared_modules/surface_tracker/surface_tracker.py b/pupil_src/shared_modules/surface_tracker/surface_tracker.py index 1ede943df4..e93dd96712 100644 --- a/pupil_src/shared_modules/surface_tracker/surface_tracker.py +++ b/pupil_src/shared_modules/surface_tracker/surface_tracker.py @@ -26,6 +26,7 @@ MarkerType, ApriltagFamily, ) +from hotkey import Hotkey logger = logging.getLogger(__name__) @@ -139,7 +140,7 @@ def init_ui(self): setter=self.on_add_surface_click, getter=lambda: False, label="A", - hotkey="a", + hotkey=Hotkey.SURFACE_TRACKER_ADD_SURFACE_CAPTURE_AND_PLAYER_HOTKEY(), ) self.g_pool.quickbar.append(self.add_button) self._update_ui() From 89dbfeaa15c95bdf909a6ef98af1124a736d80f1 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Thu, 8 Apr 2021 11:34:30 +0200 Subject: [PATCH 18/65] Annotate hotkey constants and add method for generating markdown table overview --- pupil_src/shared_modules/hotkey.py | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index b3bf20c41a..4744577160 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -15,47 +15,153 @@ class Hotkey: @staticmethod def CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(): + """Camera intrinsic estimator - collect new + + Software: Capture + """ return "i" @staticmethod def EXPORT_START_PLAYER_HOTKEY(): + """Start export + + Software: Player + """ return "e" @staticmethod def FIXATION_NEXT_PLAYER_HOTKEY(): + """Fixation - show next + + Software: Player + """ return "f" @staticmethod def FIXATION_PREV_PLAYER_HOTKEY(): + """Fixation - show previous + + Software: Player + """ return "F" @staticmethod def GAZE_CALIBRATION_CAPTURE_HOTKEY(): + """Start and stop calibration + + Software: Capture + """ return "c" @staticmethod def GAZE_VALIDATION_CAPTURE_HOTKEY(): + """Start and stop validation + + Software: Capture + """ return "t" @staticmethod def RECORDER_RUNNING_TOGGLE_CAPTURE_HOTKEY(): + """Start and stop recording + + Software: Capture + """ return "r" @staticmethod def SURFACE_TRACKER_ADD_SURFACE_CAPTURE_AND_PLAYER_HOTKEY(): + """Surface tracker - add new surface + + Software: Capture, Player + """ return "a" @staticmethod def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar + """Skip to previous frame + + Software: Player + Printable: + """ return 263 @staticmethod def SEEK_BAR_MOVE_FORWARDSS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar + """Skip to next frame + + Software: Player + Printable: + """ return 262 @staticmethod def SEEK_BAR_PLAY_PAUSE_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar + """Skip to next frame + + Software: Player + Printable: + """ return 32 + + +def generate_markdown_hotkey_docs() -> str: + import pandas as pd + + def generate_row(hotkey_id, hotkey_method): + hotkey_value = hotkey_method.__get__(Hotkey)() + hotkey_docsring = hotkey_method.__get__(Hotkey).__doc__ + doc_lines = [l.strip() for l in hotkey_docsring.split("\n") if len(l.strip())] + + if len(doc_lines) > 0: + hotkey_descr = doc_lines[0] + else: + hotkey_descr = "" + + if len(doc_lines) > 1: + hotkey_meta = dict( + tuple(map(str.strip, l.split(":"))) for l in doc_lines[1:] + ) + else: + hotkey_meta = {} + + hotkey_printable = hotkey_meta.get("Printable") + hotkey_software = sorted( + s.strip() + for s in hotkey_meta.get("Software", "").split(",") + if len(s.strip()) > 0 + ) + available_in_capture = "Capture" in hotkey_software + available_in_player = "Player" in hotkey_software + order = int(available_in_capture) + int(available_in_player) * 10 + + emoji_true = ":heavy_check_mark:" + emoji_false = ":heavy_minus_sign:" + + return { + "_ID": hotkey_id, + "_Order": order, + "Hotkey": f"`{hotkey_printable or hotkey_value}`", + "Description": hotkey_descr, + "Pupil Capture": emoji_true if available_in_capture else emoji_false, + "Pupil Player": emoji_true if available_in_player else emoji_false, + } + + hotkeys_df = pd.DataFrame( + [ + generate_row(hotkey_id, hotkey_method) + for hotkey_id, hotkey_method in vars(Hotkey).items() + if hotkey_id.endswith("_HOTKEY") + ] + ) + hotkeys_df = hotkeys_df.set_index(["Hotkey"]) + hotkeys_df = hotkeys_df.sort_values(by=["_Order", "_ID"]) + hotkeys_df = hotkeys_df[[c for c in hotkeys_df.columns if not c.startswith("_")]] + return hotkeys_df.to_markdown() + + +if __name__ == "__main__": + print(generate_markdown_hotkey_docs()) From 6f5ed3907cf779f4b88336f1297f9d9bde5c6e98 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Thu, 8 Apr 2021 11:40:34 +0200 Subject: [PATCH 19/65] Extract default annotation event hotkey --- pupil_src/shared_modules/annotations.py | 10 ++++++---- pupil_src/shared_modules/hotkey.py | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pupil_src/shared_modules/annotations.py b/pupil_src/shared_modules/annotations.py index 80c38c6db7..1c8ae136f7 100644 --- a/pupil_src/shared_modules/annotations.py +++ b/pupil_src/shared_modules/annotations.py @@ -21,6 +21,8 @@ import player_methods as pm import zmq_tools from plugin import Plugin +from hotkey import Hotkey + logger = logging.getLogger(__name__) @@ -57,20 +59,20 @@ class AnnotationPlugin(Plugin, abc.ABC): icon_chr = chr(0xE866) icon_font = "pupil_icons" - _DEFAULT_HOTKEY = "a" - def __init__(self, g_pool, annotation_definitions=None): super().__init__(g_pool) self.menu = None self._annotation_list_menu = None if annotation_definitions is None: - annotation_definitions = [["My annotation", self._DEFAULT_HOTKEY]] + annotation_definitions = [ + ["My annotation", Hotkey.ANNOTATION_EVENT_DEFAULT_HOTKEY()] + ] self._initial_annotation_definitions = annotation_definitions self._definition_to_buttons = {} self._new_annotation_label = "new annotation label" - self._new_annotation_hotkey = self._DEFAULT_HOTKEY + self._new_annotation_hotkey = Hotkey.ANNOTATION_EVENT_DEFAULT_HOTKEY() def get_init_dict(self): annotation_definitions = list(self._definition_to_buttons.keys()) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 4744577160..3a13ad9748 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -13,6 +13,14 @@ class Hotkey: """""" + @staticmethod + def ANNOTATION_EVENT_DEFAULT_HOTKEY(): + """Add annotation (default hotkey) + + Software: Capture, Player + """ + return "a" + @staticmethod def CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(): """Camera intrinsic estimator - collect new From 0b9248e8c2d38d3edce9edddfebdb2cdf2945fed Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Thu, 8 Apr 2021 11:41:48 +0200 Subject: [PATCH 20/65] Change default annotation event hotkey This avoids hotkey clash with surface tracker's "add new surface" hotkey --- pupil_src/shared_modules/hotkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 3a13ad9748..2369822919 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -19,7 +19,7 @@ def ANNOTATION_EVENT_DEFAULT_HOTKEY(): Software: Capture, Player """ - return "a" + return "x" @staticmethod def CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(): From ce7a692377baad551e8fc0c676207090c4fc633e Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Thu, 8 Apr 2021 12:08:55 +0200 Subject: [PATCH 21/65] Fix description for SEEK_BAR_PLAY_PAUSE_PLAYER_HOTKEY --- pupil_src/shared_modules/hotkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 2369822919..92d843d41b 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -108,7 +108,7 @@ def SEEK_BAR_MOVE_FORWARDSS_PLAYER_HOTKEY(): @staticmethod def SEEK_BAR_PLAY_PAUSE_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar - """Skip to next frame + """Play and pause video Software: Player Printable: From c32242c92d7e6ddab3d20dc4b2d7e3bfde074293 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 09:47:15 +0200 Subject: [PATCH 22/65] Deinit UI when cleaning plugin list for eye processes --- pupil_src/shared_modules/plugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/plugin.py b/pupil_src/shared_modules/plugin.py index 0c9dee1505..7863553413 100644 --- a/pupil_src/shared_modules/plugin.py +++ b/pupil_src/shared_modules/plugin.py @@ -471,7 +471,10 @@ def clean(self): """ for p in self._plugins[::-1]: if not p.alive: - if self.g_pool.app in ("capture", "player"): + if self.g_pool.app in ("capture", "player") or self.g_pool.process in ( + "eye0", + "eye1", + ): p.deinit_ui() p.cleanup() logger.debug("Unloaded Plugin: {}".format(p)) From 82c8f5b7e4973244306bef8d1dfa6d46a0c3e561 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 10:28:43 +0200 Subject: [PATCH 23/65] Rename Hotkey column to Keyboard Shortcut --- pupil_src/shared_modules/hotkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 92d843d41b..fca3912ffa 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -152,7 +152,7 @@ def generate_row(hotkey_id, hotkey_method): return { "_ID": hotkey_id, "_Order": order, - "Hotkey": f"`{hotkey_printable or hotkey_value}`", + "Keyboard Shortcut": f"`{hotkey_printable or hotkey_value}`", "Description": hotkey_descr, "Pupil Capture": emoji_true if available_in_capture else emoji_false, "Pupil Player": emoji_true if available_in_player else emoji_false, @@ -165,7 +165,7 @@ def generate_row(hotkey_id, hotkey_method): if hotkey_id.endswith("_HOTKEY") ] ) - hotkeys_df = hotkeys_df.set_index(["Hotkey"]) + hotkeys_df = hotkeys_df.set_index(["Keyboard Shortcut"]) hotkeys_df = hotkeys_df.sort_values(by=["_Order", "_ID"]) hotkeys_df = hotkeys_df[[c for c in hotkeys_df.columns if not c.startswith("_")]] return hotkeys_df.to_markdown() From 0990f4fd9f485205188d2d913ac5cddbf105c15a Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 10:29:45 +0200 Subject: [PATCH 24/65] Split Capture and Player hotkey tables --- pupil_src/shared_modules/hotkey.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index fca3912ffa..874d6d32b8 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -144,18 +144,17 @@ def generate_row(hotkey_id, hotkey_method): ) available_in_capture = "Capture" in hotkey_software available_in_player = "Player" in hotkey_software - order = int(available_in_capture) + int(available_in_player) * 10 - emoji_true = ":heavy_check_mark:" - emoji_false = ":heavy_minus_sign:" + default_order = int(available_in_capture) + int(available_in_player) * 10 + hotkey_order = hotkey_meta.get("Order", 1000 + default_order) return { "_ID": hotkey_id, - "_Order": order, + "_Order": hotkey_order, + "_In_Capture": available_in_capture, + "_In_Player": available_in_player, "Keyboard Shortcut": f"`{hotkey_printable or hotkey_value}`", "Description": hotkey_descr, - "Pupil Capture": emoji_true if available_in_capture else emoji_false, - "Pupil Player": emoji_true if available_in_player else emoji_false, } hotkeys_df = pd.DataFrame( @@ -167,8 +166,22 @@ def generate_row(hotkey_id, hotkey_method): ) hotkeys_df = hotkeys_df.set_index(["Keyboard Shortcut"]) hotkeys_df = hotkeys_df.sort_values(by=["_Order", "_ID"]) - hotkeys_df = hotkeys_df[[c for c in hotkeys_df.columns if not c.startswith("_")]] - return hotkeys_df.to_markdown() + + # Only show columns that don't start with an underscore + visible_columns = [c for c in hotkeys_df.columns if not c.startswith("_")] + + capture_df = hotkeys_df[hotkeys_df["_In_Capture"] == True] + player_df = hotkeys_df[hotkeys_df["_In_Player"] == True] + + capture_title_md = "# Pupil Capture" + capture_table_md = capture_df[visible_columns].to_markdown() + + player_title_md = "# Pupil Player" + player_table_md = player_df[visible_columns].to_markdown() + + return "\n" + "\n\n".join( + [capture_title_md, capture_table_md, player_title_md, player_table_md] + ) if __name__ == "__main__": From 05255b91eaceae4465037db02ad6de4eedcbc514 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 10:32:03 +0200 Subject: [PATCH 25/65] Update camera intrinsic estimation hotkey description --- pupil_src/shared_modules/hotkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 874d6d32b8..db28054893 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -23,7 +23,7 @@ def ANNOTATION_EVENT_DEFAULT_HOTKEY(): @staticmethod def CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(): - """Camera intrinsic estimator - collect new + """Camera intrinsic estimation: Take snapshot of circle pattern Software: Capture """ From 06cab5db903d718d960ad55d6313e01463a2e8c0 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 10:32:20 +0200 Subject: [PATCH 26/65] Update hotkey descriptions to make them more consistent --- pupil_src/shared_modules/hotkey.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index db28054893..62170b0829 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -39,7 +39,7 @@ def EXPORT_START_PLAYER_HOTKEY(): @staticmethod def FIXATION_NEXT_PLAYER_HOTKEY(): - """Fixation - show next + """Fixation: Show next Software: Player """ @@ -47,7 +47,7 @@ def FIXATION_NEXT_PLAYER_HOTKEY(): @staticmethod def FIXATION_PREV_PLAYER_HOTKEY(): - """Fixation - show previous + """Fixation: Show previous Software: Player """ @@ -79,7 +79,7 @@ def RECORDER_RUNNING_TOGGLE_CAPTURE_HOTKEY(): @staticmethod def SURFACE_TRACKER_ADD_SURFACE_CAPTURE_AND_PLAYER_HOTKEY(): - """Surface tracker - add new surface + """Surface tracker: Add new surface Software: Capture, Player """ From f1eaf6026167a128da098be94ef09207e3e8945e Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 10:38:07 +0200 Subject: [PATCH 27/65] Replace hotkey with keyboard shortcut in hotkey descriptions --- pupil_src/shared_modules/hotkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 62170b0829..f7a6f2e10a 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -15,7 +15,7 @@ class Hotkey: @staticmethod def ANNOTATION_EVENT_DEFAULT_HOTKEY(): - """Add annotation (default hotkey) + """Add annotation (default keyboard shortcut) Software: Capture, Player """ From 1ea0befd101baa547c85342c50a3366defed171e Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 11:04:30 +0200 Subject: [PATCH 28/65] Update hotkey printable labels --- pupil_src/shared_modules/hotkey.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index f7a6f2e10a..ac9f78436c 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -91,7 +91,7 @@ def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): """Skip to previous frame Software: Player - Printable: + Printable: """ return 263 @@ -101,7 +101,7 @@ def SEEK_BAR_MOVE_FORWARDSS_PLAYER_HOTKEY(): """Skip to next frame Software: Player - Printable: + Printable: """ return 262 @@ -111,7 +111,7 @@ def SEEK_BAR_PLAY_PAUSE_PLAYER_HOTKEY(): """Play and pause video Software: Player - Printable: + Printable: """ return 32 From d607ed4c61ee101a23fe099a95b9838a9b98bfd8 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 11:04:56 +0200 Subject: [PATCH 29/65] Add software specific order for hotkey tables --- pupil_src/shared_modules/hotkey.py | 51 ++++++++++++++---------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index ac9f78436c..a2dcc5fe3d 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -17,7 +17,8 @@ class Hotkey: def ANNOTATION_EVENT_DEFAULT_HOTKEY(): """Add annotation (default keyboard shortcut) - Software: Capture, Player + Capture Order: 40 + Player Order: 50 """ return "x" @@ -25,7 +26,7 @@ def ANNOTATION_EVENT_DEFAULT_HOTKEY(): def CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(): """Camera intrinsic estimation: Take snapshot of circle pattern - Software: Capture + Capture Order: 50 """ return "i" @@ -33,7 +34,7 @@ def CAMERA_INTRINSIC_ESTIMATOR_COLLECT_NEW_CAPTURE_HOTKEY(): def EXPORT_START_PLAYER_HOTKEY(): """Start export - Software: Player + Player Order: 30 """ return "e" @@ -41,7 +42,7 @@ def EXPORT_START_PLAYER_HOTKEY(): def FIXATION_NEXT_PLAYER_HOTKEY(): """Fixation: Show next - Software: Player + Player Order: 60 """ return "f" @@ -49,7 +50,7 @@ def FIXATION_NEXT_PLAYER_HOTKEY(): def FIXATION_PREV_PLAYER_HOTKEY(): """Fixation: Show previous - Software: Player + Player Order: 61 """ return "F" @@ -57,7 +58,7 @@ def FIXATION_PREV_PLAYER_HOTKEY(): def GAZE_CALIBRATION_CAPTURE_HOTKEY(): """Start and stop calibration - Software: Capture + Capture Order: 20 """ return "c" @@ -65,7 +66,7 @@ def GAZE_CALIBRATION_CAPTURE_HOTKEY(): def GAZE_VALIDATION_CAPTURE_HOTKEY(): """Start and stop validation - Software: Capture + Capture Order: 21 """ return "t" @@ -73,7 +74,7 @@ def GAZE_VALIDATION_CAPTURE_HOTKEY(): def RECORDER_RUNNING_TOGGLE_CAPTURE_HOTKEY(): """Start and stop recording - Software: Capture + Capture Order: 10 """ return "r" @@ -81,7 +82,8 @@ def RECORDER_RUNNING_TOGGLE_CAPTURE_HOTKEY(): def SURFACE_TRACKER_ADD_SURFACE_CAPTURE_AND_PLAYER_HOTKEY(): """Surface tracker: Add new surface - Software: Capture, Player + Capture Order: 30 + Player Order: 40 """ return "a" @@ -90,8 +92,8 @@ def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar """Skip to previous frame - Software: Player Printable: + Player Order: 20 """ return 263 @@ -100,8 +102,8 @@ def SEEK_BAR_MOVE_FORWARDSS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar """Skip to next frame - Software: Player Printable: + Player Order: 21 """ return 262 @@ -110,8 +112,8 @@ def SEEK_BAR_PLAY_PAUSE_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar """Play and pause video - Software: Player Printable: + Player Order: 10 """ return 32 @@ -137,22 +139,13 @@ def generate_row(hotkey_id, hotkey_method): hotkey_meta = {} hotkey_printable = hotkey_meta.get("Printable") - hotkey_software = sorted( - s.strip() - for s in hotkey_meta.get("Software", "").split(",") - if len(s.strip()) > 0 - ) - available_in_capture = "Capture" in hotkey_software - available_in_player = "Player" in hotkey_software - - default_order = int(available_in_capture) + int(available_in_player) * 10 - hotkey_order = hotkey_meta.get("Order", 1000 + default_order) + hotkey_order_in_capture = hotkey_meta.get("Capture Order", None) + hotkey_order_in_player = hotkey_meta.get("Player Order", None) return { "_ID": hotkey_id, - "_Order": hotkey_order, - "_In_Capture": available_in_capture, - "_In_Player": available_in_player, + "_Order_In_Capture": hotkey_order_in_capture, + "_Order_In_Player": hotkey_order_in_player, "Keyboard Shortcut": f"`{hotkey_printable or hotkey_value}`", "Description": hotkey_descr, } @@ -165,13 +158,15 @@ def generate_row(hotkey_id, hotkey_method): ] ) hotkeys_df = hotkeys_df.set_index(["Keyboard Shortcut"]) - hotkeys_df = hotkeys_df.sort_values(by=["_Order", "_ID"]) # Only show columns that don't start with an underscore visible_columns = [c for c in hotkeys_df.columns if not c.startswith("_")] - capture_df = hotkeys_df[hotkeys_df["_In_Capture"] == True] - player_df = hotkeys_df[hotkeys_df["_In_Player"] == True] + capture_df = hotkeys_df[hotkeys_df["_Order_In_Capture"].notnull()] + capture_df = capture_df.sort_values(by=["_Order_In_Capture"]) + + player_df = hotkeys_df[hotkeys_df["_Order_In_Player"].notnull()] + player_df = player_df.sort_values(by=["_Order_In_Player"]) capture_title_md = "# Pupil Capture" capture_table_md = capture_df[visible_columns].to_markdown() From 8a5f48375d5e9ee306255a1961ec9e70a2a87975 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 12 Apr 2021 11:07:27 +0200 Subject: [PATCH 30/65] Update hotkey arrow left/right descriptions --- pupil_src/shared_modules/hotkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index a2dcc5fe3d..50fb43dc42 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -90,7 +90,7 @@ def SURFACE_TRACKER_ADD_SURFACE_CAPTURE_AND_PLAYER_HOTKEY(): @staticmethod def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar - """Skip to previous frame + """Step to previous frame / Decrease playback speed Printable: Player Order: 20 @@ -100,7 +100,7 @@ def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): @staticmethod def SEEK_BAR_MOVE_FORWARDSS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar - """Skip to next frame + """Step to next frame / Increase playback speed Printable: Player Order: 21 From 3d50d77330c830e75000f9749250486220dbbafd Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 13 Apr 2021 10:10:45 +0200 Subject: [PATCH 31/65] Extract draw_circle_filled method --- .../controller/marker_window_controller.py | 44 +++------------- pupil_src/shared_modules/gl_utils/__init__.py | 1 + pupil_src/shared_modules/gl_utils/draw.py | 50 +++++++++++++++++++ 3 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 pupil_src/shared_modules/gl_utils/draw.py diff --git a/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py b/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py index 4c658ec535..1e2f3d3ad9 100644 --- a/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py +++ b/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py @@ -22,6 +22,8 @@ from pyglui.cygl.utils import draw_points from pyglui.cygl.utils import RGBA +from gl_utils import draw_circle_filled + from .gui_monitor import GUIMonitor from .gui_window import GUIWindow @@ -316,22 +318,22 @@ def __draw_circle_marker( # TODO: adjust num_points such that circles look smooth; smaller circles need less points # TODO: compare runtimes with `draw_points` - _draw_circle_filled( + draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_OUTER * radius, color=RGBA(*self._MARKER_CIRCLE_RGB_OUTER, alpha), ) - _draw_circle_filled( + draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_MIDDLE * radius, color=RGBA(*self._MARKER_CIRCLE_RGB_MIDDLE, alpha), ) - _draw_circle_filled( + draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_INNER * radius, color=RGBA(*self._MARKER_CIRCLE_RGB_INNER, alpha), ) - _draw_circle_filled( + draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_FEEDBACK * radius, color=RGBA(*marker_circle_rgb_feedback, alpha), @@ -528,37 +530,3 @@ def _interp_fn(t, b, c, d, start_sample=15.0, stop_sample=55.0): return 1 - _easeInOutQuad(t - stop_sample, b, c, d - stop_sample) else: return 1.0 - - -@functools.lru_cache(4) # 4 circles needed to draw calibration marker -def _circle_points_around_zero(radius: float, num_points: int) -> np.ndarray: - t = np.linspace(0, 2 * np.pi, num_points, dtype=np.float64) - t.shape = -1, 1 - points = np.hstack([np.cos(t), np.sin(t)]) - points *= radius - return points - - -@functools.lru_cache(4) # 4 circles needed to draw calibration marker -def _circle_points_offset( - offset: T.Tuple[float, float], radius: float, num_points: int, flat: bool = True -) -> np.ndarray: - # NOTE: .copy() to avoid modifying the cached result - points = _circle_points_around_zero(radius, num_points).copy() - points[:, 0] += offset[0] - points[:, 1] += offset[1] - if flat: - points.shape = -1 - return points - - -def _draw_circle_filled( - screen_point: T.Tuple[float, float], size: float, color: RGBA, num_points: int = 50 -): - points = _circle_points_offset( - screen_point, radius=size, num_points=num_points, flat=False - ) - gl.glColor4f(color.r, color.g, color.b, color.a) - gl.glEnableClientState(gl.GL_VERTEX_ARRAY) - gl.glVertexPointer(2, gl.GL_DOUBLE, 0, points) - gl.glDrawArrays(gl.GL_POLYGON, 0, points.shape[0]) diff --git a/pupil_src/shared_modules/gl_utils/__init__.py b/pupil_src/shared_modules/gl_utils/__init__.py index 9b197532b0..76ae4a2e2e 100644 --- a/pupil_src/shared_modules/gl_utils/__init__.py +++ b/pupil_src/shared_modules/gl_utils/__init__.py @@ -8,6 +8,7 @@ See COPYING and COPYING.LESSER for license details. ---------------------------------------------------------------------------~(*) """ +from .draw import draw_circle_filled from .trackball import Trackball from .utils import ( _Rectangle, diff --git a/pupil_src/shared_modules/gl_utils/draw.py b/pupil_src/shared_modules/gl_utils/draw.py new file mode 100644 index 0000000000..547c0f2c2b --- /dev/null +++ b/pupil_src/shared_modules/gl_utils/draw.py @@ -0,0 +1,50 @@ +""" +(*)~--------------------------------------------------------------------------- +Pupil - eye tracking platform +Copyright (C) 2012-2021 Pupil Labs + +Distributed under the terms of the GNU +Lesser General Public License (LGPL v3.0). +See COPYING and COPYING.LESSER for license details. +---------------------------------------------------------------------------~(*) +""" +import functools +import typing as T + +import numpy as np +import OpenGL.GL as gl +from pyglui.cygl.utils import RGBA + + +def draw_circle_filled( + screen_point: T.Tuple[float, float], size: float, color: RGBA, num_points: int = 50 +): + points = _circle_points_offset( + screen_point, radius=size, num_points=num_points, flat=False + ) + gl.glColor4f(color.r, color.g, color.b, color.a) + gl.glEnableClientState(gl.GL_VERTEX_ARRAY) + gl.glVertexPointer(2, gl.GL_DOUBLE, 0, points) + gl.glDrawArrays(gl.GL_POLYGON, 0, points.shape[0]) + + +@functools.lru_cache(4) # 4 circles needed to draw calibration marker +def _circle_points_offset( + offset: T.Tuple[float, float], radius: float, num_points: int, flat: bool = True +) -> np.ndarray: + # NOTE: .copy() to avoid modifying the cached result + points = _circle_points_around_zero(radius, num_points).copy() + points[:, 0] += offset[0] + points[:, 1] += offset[1] + if flat: + points.shape = -1 + return points + + +@functools.lru_cache(4) # 4 circles needed to draw calibration marker +def _circle_points_around_zero(radius: float, num_points: int) -> np.ndarray: + t = np.linspace(0, 2 * np.pi, num_points, dtype=np.float64) + t.shape = -1, 1 + points = np.hstack([np.cos(t), np.sin(t)]) + points *= radius + return points From 0ed4b46bdeaa4c1fd4b7aa88ec772ea345674359 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 13 Apr 2021 10:13:06 +0200 Subject: [PATCH 32/65] Update Display_Recent_Gaze to draw gaze point with filled polygon --- pupil_src/shared_modules/display_recent_gaze.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/display_recent_gaze.py b/pupil_src/shared_modules/display_recent_gaze.py index ff54caaa33..236577d8d8 100644 --- a/pupil_src/shared_modules/display_recent_gaze.py +++ b/pupil_src/shared_modules/display_recent_gaze.py @@ -11,6 +11,8 @@ from plugin import System_Plugin_Base from pyglui.cygl.utils import draw_points_norm, RGBA +from gl_utils import draw_circle_filled +from methods import denormalize class Display_Recent_Gaze(System_Plugin_Base): @@ -25,14 +27,23 @@ def __init__(self, g_pool): self.pupil_display_list = [] def recent_events(self, events): + frame = events.get("frame", None) + if not frame: + return + frame_size = frame.width, frame.height for pt in events.get("gaze", []): - self.pupil_display_list.append((pt["norm_pos"], pt["confidence"] * 0.8)) + point = denormalize(pt["norm_pos"], frame_size, flip_y=True) + self.pupil_display_list.append((point, pt["confidence"] * 0.8)) self.pupil_display_list[:-3] = [] def gl_display(self): for pt, a in self.pupil_display_list: # This could be faster if there would be a method to also add multiple colors per point - draw_points_norm([pt], size=35, color=RGBA(1.0, 0.2, 0.4, a)) + draw_circle_filled( + tuple(pt), + size=35 / 2, + color=RGBA(1.0, 0.2, 0.4, a), + ) def get_init_dict(self): return {} From 223ed3bc6a9930ce7f6ee7f689d36c022104e9a2 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 14 Apr 2021 09:43:45 +0200 Subject: [PATCH 33/65] Add docstring to read_info_csv_file and read_info_json_file --- .../shared_modules/pupil_recording/info/recording_info_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py b/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py index 1b8c78c641..2af4f0d9f2 100644 --- a/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py +++ b/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py @@ -79,12 +79,14 @@ def f(value): def read_info_csv_file(rec_dir: str) -> dict: + """Read `info.csv` file from recording.""" file_path = os.path.join(rec_dir, "info.csv") with open(file_path, "r") as file: return csv_utils.read_key_value_file(file) def read_info_json_file(rec_dir: str) -> dict: + """Read `info.json` file from recording.""" file_path = os.path.join(rec_dir, "info.json") with open(file_path, "r") as file: return json.load(file) From a194b508758c9dd879db20f243db298365e78d04 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 14 Apr 2021 09:44:02 +0200 Subject: [PATCH 34/65] Add read_info_invisible_json_file method --- .../pupil_recording/info/recording_info_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py b/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py index 2af4f0d9f2..6bd54d7d26 100644 --- a/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py +++ b/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py @@ -92,6 +92,13 @@ def read_info_json_file(rec_dir: str) -> dict: return json.load(file) +def read_info_invisible_json_file(rec_dir: str) -> dict: + """Read `info.invisible.json` file from recording.""" + file_path = os.path.join(rec_dir, "info.invisible.json") + with open(file_path, "r") as file: + return json.load(file) + + def parse_duration_string(duration_string: str) -> int: """Returns number of seconds from string 'HH:MM:SS'.""" H, M, S = [int(part) for part in duration_string.split(":")] From 999e7ea9ef5e21f76569b277e07eaf6ed10a217d Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 14 Apr 2021 09:44:21 +0200 Subject: [PATCH 35/65] Add read_pupil_invisible_info_file method --- .../pupil_recording/info/recording_info_utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py b/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py index 6bd54d7d26..a953b77702 100644 --- a/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py +++ b/pupil_src/shared_modules/pupil_recording/info/recording_info_utils.py @@ -99,6 +99,14 @@ def read_info_invisible_json_file(rec_dir: str) -> dict: return json.load(file) +def read_pupil_invisible_info_file(rec_dir: str) -> dict: + """Read info file from Pupil Invisible recording.""" + try: + return read_info_json_file(rec_dir) + except FileNotFoundError: + return read_info_invisible_json_file(rec_dir) + + def parse_duration_string(duration_string: str) -> int: """Returns number of seconds from string 'HH:MM:SS'.""" H, M, S = [int(part) for part in duration_string.split(":")] From 3f83851ef4ad3230c922652e7379b21aba2dcb5a Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 14 Apr 2021 09:44:50 +0200 Subject: [PATCH 36/65] Use original start_time_synced_ns when rewriting invisible timestamps --- .../shared_modules/pupil_recording/update/invisible.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/pupil_recording/update/invisible.py b/pupil_src/shared_modules/pupil_recording/update/invisible.py index 4926fdf54c..b8f84d4871 100644 --- a/pupil_src/shared_modules/pupil_recording/update/invisible.py +++ b/pupil_src/shared_modules/pupil_recording/update/invisible.py @@ -137,14 +137,18 @@ def _pi_path_core_path_pairs(recording: PupilRecording): def _rewrite_timestamps(recording: PupilRecording): - start_time = recording.meta_info.start_time_synced_ns + + # Use start time from info file (instead of recording.meta_info.start_time_synced_ns) + # to have a more precise value and avoid having a negative first timestamp when rewriting + info_json = utils.read_pupil_invisible_info_file(recording.rec_dir) + start_time_synced_ns = int(info_json["start_time"]) def conversion(timestamps: np.array): # Subtract start_time from all times in the recording, so timestamps # start at 0. This is to increase precision when converting # timestamps to float32, e.g. for OpenGL! SECONDS_PER_NANOSECOND = 1e-9 - return (timestamps - start_time) * SECONDS_PER_NANOSECOND + return (timestamps - start_time_synced_ns) * SECONDS_PER_NANOSECOND update_utils._rewrite_times(recording, dtype=" Date: Wed, 14 Apr 2021 10:26:58 +0200 Subject: [PATCH 37/65] Fix typo in SEEK_BAR_MOVE_FORWARDS_PLAYER_HOTKEY name --- pupil_src/shared_modules/hotkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 50fb43dc42..7e5dfb2739 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -98,7 +98,7 @@ def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): return 263 @staticmethod - def SEEK_BAR_MOVE_FORWARDSS_PLAYER_HOTKEY(): + def SEEK_BAR_MOVE_FORWARDS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar """Step to next frame / Increase playback speed From 234573cfec9f25931e4b49fb1d049541fc5fba32 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 14 Apr 2021 10:27:24 +0200 Subject: [PATCH 38/65] Add footnote to player hotkey overview --- pupil_src/shared_modules/hotkey.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/hotkey.py b/pupil_src/shared_modules/hotkey.py index 7e5dfb2739..5446ff5710 100644 --- a/pupil_src/shared_modules/hotkey.py +++ b/pupil_src/shared_modules/hotkey.py @@ -90,7 +90,7 @@ def SURFACE_TRACKER_ADD_SURFACE_CAPTURE_AND_PLAYER_HOTKEY(): @staticmethod def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar - """Step to previous frame / Decrease playback speed + """Step to previous frame\\* / Decrease playback speed\\*\\* Printable: Player Order: 20 @@ -100,7 +100,7 @@ def SEEK_BAR_MOVE_BACKWARDS_PLAYER_HOTKEY(): @staticmethod def SEEK_BAR_MOVE_FORWARDS_PLAYER_HOTKEY(): # This is only implicitly used by pyglui.ui.Seek_Bar - """Step to next frame / Increase playback speed + """Step to next frame\\* / Increase playback speed\\*\\* Printable: Player Order: 21 @@ -174,8 +174,16 @@ def generate_row(hotkey_id, hotkey_method): player_title_md = "# Pupil Player" player_table_md = player_df[visible_columns].to_markdown() + player_footnote = "\\* While paused\n\\* During playback" + return "\n" + "\n\n".join( - [capture_title_md, capture_table_md, player_title_md, player_table_md] + [ + capture_title_md, + capture_table_md, + player_title_md, + player_table_md, + player_footnote, + ] ) From 38814f4388c0dec0fd70a66f406576d8e7bfbc7a Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Sun, 18 Apr 2021 23:41:48 +0200 Subject: [PATCH 39/65] Extract pye3d plugin visualization color constants --- .../pupil_detector_plugins/pye3d_plugin.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py index 34ad04a596..68fa3a8b99 100644 --- a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py +++ b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py @@ -41,6 +41,11 @@ class Pye3DPlugin(PupilDetectorPlugin): icon_chr = chr(0xEC19) order = 0.101 + # Visualization + COLOR_ULTRA_LONG_TERM_MODEL = (0.5, 0, 0, 1) + COLOR_LONG_TERM_MODEL = (0.8, 0.8, 0, 1) + COLOR_SHORT_TERM_MODEL = (0, 1, 0, 1) + @property def pupil_detector(self): return self.detector @@ -198,17 +203,17 @@ def gl_display(self): debug_info = result["debug_info"] draw_ellipse( ellipse=debug_info["projected_ultra_long_term"], - rgba=(0.5, 0, 0, 1), + rgba=self.COLOR_ULTRA_LONG_TERM_MODEL, thickness=2, ) draw_ellipse( ellipse=debug_info["projected_long_term"], - rgba=(0.8, 0.8, 0, 1), + rgba=self.COLOR_LONG_TERM_MODEL, thickness=2, ) draw_ellipse( ellipse=debug_info["projected_short_term"], - rgba=(0, 1, 0, 1), + rgba=self.COLOR_SHORT_TERM_MODEL, thickness=2, ) if self.__debug_window_button: From 7e5c6bea9a7a662dc7188719395327c45686ccfb Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Sun, 18 Apr 2021 23:46:46 +0200 Subject: [PATCH 40/65] Update pye3d plugin visualization colors --- .../shared_modules/pupil_detector_plugins/pye3d_plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py index 68fa3a8b99..ee8f0fdcce 100644 --- a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py +++ b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py @@ -42,9 +42,9 @@ class Pye3DPlugin(PupilDetectorPlugin): order = 0.101 # Visualization - COLOR_ULTRA_LONG_TERM_MODEL = (0.5, 0, 0, 1) - COLOR_LONG_TERM_MODEL = (0.8, 0.8, 0, 1) - COLOR_SHORT_TERM_MODEL = (0, 1, 0, 1) + COLOR_ULTRA_LONG_TERM_MODEL = (0.5, 0, 0.5, 1) # purple + COLOR_LONG_TERM_MODEL = (0, 0.9, 0.1, 1) # green + COLOR_SHORT_TERM_MODEL = (0.8, 0.8, 0, 1) # yellow @property def pupil_detector(self): From 13e78902f6ba9e257f8e094bbb91d67472c9a575 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Sun, 18 Apr 2021 23:50:02 +0200 Subject: [PATCH 41/65] Always draw pye3d plugin's pupil outline --- .../pupil_detector_plugins/pye3d_plugin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py index ee8f0fdcce..469a43bf8d 100644 --- a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py +++ b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py @@ -192,14 +192,14 @@ def init_ui(self): def gl_display(self): self.debug_window_update() result = self._recent_detection_result + if result is not None: if not self.is_debug_window_open: - # normal drawing + # normal eyeball drawing draw_eyeball_outline(result) - draw_pupil_outline(result) elif "debug_info" in result: - # debug drawing + # debug eyeball drawing debug_info = result["debug_info"] draw_ellipse( ellipse=debug_info["projected_ultra_long_term"], @@ -216,6 +216,10 @@ def gl_display(self): rgba=self.COLOR_SHORT_TERM_MODEL, thickness=2, ) + + # always draw pupil + draw_pupil_outline(result) + if self.__debug_window_button: self.__debug_window_button.label = self.__debug_window_button_label From 16063581ef6b11a5e9b167ebc42a37cba42a7835 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 20 Apr 2021 10:33:26 +0200 Subject: [PATCH 42/65] Add is_valid property to AccuracyPrecisionResult --- pupil_src/shared_modules/accuracy_visualizer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index 92b3bf79c6..2e00c78e35 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -58,6 +58,13 @@ def empty() -> "CorrelatedAndCoordinateTransformedResult": camera_space=np.ndarray([]), ) + @property + def is_valid(self) -> bool: + if len(self.norm_space.shape) != 2: + return False + # TODO: Make validity check exhaustive + return True + class CorrelationError(ValueError): pass @@ -78,6 +85,13 @@ def failed() -> "AccuracyPrecisionResult": correlation=CorrelatedAndCoordinateTransformedResult.empty(), ) + @property + def is_valid(self) -> bool: + if not self.correlation.is_valid: + return False + # TODO: Make validity check exhaustive + return True + class ValidationInput: def __init__(self): From 88606ab0ffaa0e03c9b7dbc93f068b48d502ff48 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 20 Apr 2021 10:34:18 +0200 Subject: [PATCH 43/65] Early exit if AccuracyPrecisionResult is not valid in Accuracy_Visualizer --- pupil_src/shared_modules/accuracy_visualizer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index 2e00c78e35..23772d03ae 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -385,6 +385,9 @@ def recalculate(self): succession_threshold=self.succession_threshold, ) + if not results.is_valid: + return + accuracy = results.accuracy.result if np.isnan(accuracy): self.accuracy = None From 89099e351ef21c0d27e05a1b91811292dad3d69e Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 20 Apr 2021 11:53:27 +0200 Subject: [PATCH 44/65] Cache frame size in Display_Recent_Gaze --- pupil_src/shared_modules/display_recent_gaze.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/display_recent_gaze.py b/pupil_src/shared_modules/display_recent_gaze.py index 236577d8d8..0781a9bf7e 100644 --- a/pupil_src/shared_modules/display_recent_gaze.py +++ b/pupil_src/shared_modules/display_recent_gaze.py @@ -25,15 +25,23 @@ def __init__(self, g_pool): super().__init__(g_pool) self.order = 0.8 self.pupil_display_list = [] + self.__recent_frame_size = None def recent_events(self, events): frame = events.get("frame", None) - if not frame: + + if frame: + # Save/update current frame size whenever a frame is available + self.__recent_frame_size = (frame.width, frame.height) + + if not self.__recent_frame_size: + # If there wasn't any frame yet to get the size from - return return - frame_size = frame.width, frame.height + for pt in events.get("gaze", []): - point = denormalize(pt["norm_pos"], frame_size, flip_y=True) + point = denormalize(pt["norm_pos"], self.__recent_frame_size, flip_y=True) self.pupil_display_list.append((point, pt["confidence"] * 0.8)) + self.pupil_display_list[:-3] = [] def gl_display(self): From 5809f3be33dde152833b47f57153daa6af1eabbd Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 20 Apr 2021 22:32:34 +0200 Subject: [PATCH 45/65] Add info message when not enough data is collected for gaze accuracy estimation Co-authored-by: Pablo Prietz --- pupil_src/shared_modules/accuracy_visualizer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index 23772d03ae..61fe6a3253 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -386,6 +386,9 @@ def recalculate(self): ) if not results.is_valid: + logger.info( + "Did not collect enough data to estimate gaze mapping accuracy." + ) return accuracy = results.accuracy.result From 6a3f3d2c94300edd8a021f9e5d74eb93f516ad06 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Sun, 25 Apr 2021 23:09:01 +0200 Subject: [PATCH 46/65] Activate main_window when calling consume_events_and_render_buffer --- pupil_src/launchables/world.py | 3 ++- pupil_src/shared_modules/gl_utils/__init__.py | 1 + pupil_src/shared_modules/gl_utils/utils.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pupil_src/launchables/world.py b/pupil_src/launchables/world.py index ff3f88cdf2..727a8c11aa 100644 --- a/pupil_src/launchables/world.py +++ b/pupil_src/launchables/world.py @@ -417,7 +417,8 @@ def on_resize(window, w, h): ) # Needed, to update the window buffer while resizing - consume_events_and_render_buffer() + with gl_utils.current_context(main_window): + consume_events_and_render_buffer() def on_window_key(window, key, scancode, action, mods): g_pool.gui.update_key(key, scancode, action, mods) diff --git a/pupil_src/shared_modules/gl_utils/__init__.py b/pupil_src/shared_modules/gl_utils/__init__.py index 76ae4a2e2e..a19a7fbfa3 100644 --- a/pupil_src/shared_modules/gl_utils/__init__.py +++ b/pupil_src/shared_modules/gl_utils/__init__.py @@ -16,6 +16,7 @@ basic_gl_setup, clear_gl_screen, Coord_System, + current_context, cvmat_to_glmat, get_content_scale, get_framebuffer_scale, diff --git a/pupil_src/shared_modules/gl_utils/utils.py b/pupil_src/shared_modules/gl_utils/utils.py index 956b05089b..a0690919d7 100644 --- a/pupil_src/shared_modules/gl_utils/utils.py +++ b/pupil_src/shared_modules/gl_utils/utils.py @@ -331,6 +331,16 @@ def get_window_title_bar_rect(window) -> _Rectangle: ) +@contextlib.contextmanager +def current_context(window): + prev_context = glfw.get_current_context() + glfw.make_context_current(window) + try: + yield + finally: + glfw.make_context_current(prev_context) + + _GLFWErrorReportingDict = T.Dict[T.Union[None, int], str] From c02d278190e33e8f5927aea6a9c500155e3251f7 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Sun, 25 Apr 2021 23:14:52 +0200 Subject: [PATCH 47/65] Use g_pool.capture.frame_size instead of caching frame.width and frame.height --- pupil_src/shared_modules/display_recent_gaze.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pupil_src/shared_modules/display_recent_gaze.py b/pupil_src/shared_modules/display_recent_gaze.py index 0781a9bf7e..2855283a75 100644 --- a/pupil_src/shared_modules/display_recent_gaze.py +++ b/pupil_src/shared_modules/display_recent_gaze.py @@ -25,21 +25,15 @@ def __init__(self, g_pool): super().__init__(g_pool) self.order = 0.8 self.pupil_display_list = [] - self.__recent_frame_size = None def recent_events(self, events): frame = events.get("frame", None) - - if frame: - # Save/update current frame size whenever a frame is available - self.__recent_frame_size = (frame.width, frame.height) - - if not self.__recent_frame_size: - # If there wasn't any frame yet to get the size from - return + if not frame: return for pt in events.get("gaze", []): - point = denormalize(pt["norm_pos"], self.__recent_frame_size, flip_y=True) + recent_frame_size = self.g_pool.capture.frame_size + point = denormalize(pt["norm_pos"], recent_frame_size, flip_y=True) self.pupil_display_list.append((point, pt["confidence"] * 0.8)) self.pupil_display_list[:-3] = [] From 8f78e741297170fbd6cdafa6228d9c7b40fde4dc Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Sun, 25 Apr 2021 23:22:02 +0200 Subject: [PATCH 48/65] Change not enough data for validation log to warning --- pupil_src/shared_modules/accuracy_visualizer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index 61fe6a3253..6c1d8d96ed 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -368,10 +368,12 @@ def __handle_validation_data_notification(self, note_dict: dict) -> bool: return True def recalculate(self): + NOT_ENOUGH_DATA_COLLECTED_ERR_MSG = ( + "Did not collect enough data to estimate gaze mapping accuracy." + ) + if not self.recent_input.is_complete: - logger.info( - "Did not collect enough data to estimate gaze mapping accuracy." - ) + logger.warning(NOT_ENOUGH_DATA_COLLECTED_ERR_MSG) return results = self.calc_acc_prec_errlines( @@ -386,9 +388,7 @@ def recalculate(self): ) if not results.is_valid: - logger.info( - "Did not collect enough data to estimate gaze mapping accuracy." - ) + logger.warning(NOT_ENOUGH_DATA_COLLECTED_ERR_MSG) return accuracy = results.accuracy.result From 5ae826b6b14f4b743f1527badee756b5968cb53a Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 28 Apr 2021 09:34:44 +0200 Subject: [PATCH 49/65] Remove getting the frame from recent events in Display_Recent_Gaze --- pupil_src/shared_modules/display_recent_gaze.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pupil_src/shared_modules/display_recent_gaze.py b/pupil_src/shared_modules/display_recent_gaze.py index 2855283a75..bd24260cae 100644 --- a/pupil_src/shared_modules/display_recent_gaze.py +++ b/pupil_src/shared_modules/display_recent_gaze.py @@ -27,10 +27,6 @@ def __init__(self, g_pool): self.pupil_display_list = [] def recent_events(self, events): - frame = events.get("frame", None) - if not frame: - return - for pt in events.get("gaze", []): recent_frame_size = self.g_pool.capture.frame_size point = denormalize(pt["norm_pos"], recent_frame_size, flip_y=True) From 78324289e518602f9e4319e7ce4c9a9fb18bf103 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 28 Apr 2021 09:35:35 +0200 Subject: [PATCH 50/65] Replace draw_circle_filled with draw_circle_filled_func_builder --- .../controller/marker_window_controller.py | 12 +++-- .../shared_modules/display_recent_gaze.py | 5 +- pupil_src/shared_modules/gl_utils/__init__.py | 2 +- pupil_src/shared_modules/gl_utils/draw.py | 48 +++++++++++++++---- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py b/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py index 1e2f3d3ad9..f5e3243d33 100644 --- a/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py +++ b/pupil_src/shared_modules/calibration_choreography/controller/marker_window_controller.py @@ -22,7 +22,7 @@ from pyglui.cygl.utils import draw_points from pyglui.cygl.utils import RGBA -from gl_utils import draw_circle_filled +from gl_utils import draw_circle_filled_func_builder from .gui_monitor import GUIMonitor from .gui_window import GUIWindow @@ -103,6 +103,8 @@ def __init__(self, marker_scale: float): self.__glfont.set_size(32) self.__glfont.set_color_float((0.2, 0.5, 0.9, 1.0)) self.__glfont.set_align_string(v_align="center") + # Private helper + self.__draw_circle_filled = draw_circle_filled_func_builder(cache_size=4) # Public - Marker Management @@ -318,22 +320,22 @@ def __draw_circle_marker( # TODO: adjust num_points such that circles look smooth; smaller circles need less points # TODO: compare runtimes with `draw_points` - draw_circle_filled( + self.__draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_OUTER * radius, color=RGBA(*self._MARKER_CIRCLE_RGB_OUTER, alpha), ) - draw_circle_filled( + self.__draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_MIDDLE * radius, color=RGBA(*self._MARKER_CIRCLE_RGB_MIDDLE, alpha), ) - draw_circle_filled( + self.__draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_INNER * radius, color=RGBA(*self._MARKER_CIRCLE_RGB_INNER, alpha), ) - draw_circle_filled( + self.__draw_circle_filled( screen_point, size=self._MARKER_CIRCLE_SIZE_FEEDBACK * radius, color=RGBA(*marker_circle_rgb_feedback, alpha), diff --git a/pupil_src/shared_modules/display_recent_gaze.py b/pupil_src/shared_modules/display_recent_gaze.py index bd24260cae..6e6ef0ace2 100644 --- a/pupil_src/shared_modules/display_recent_gaze.py +++ b/pupil_src/shared_modules/display_recent_gaze.py @@ -11,7 +11,7 @@ from plugin import System_Plugin_Base from pyglui.cygl.utils import draw_points_norm, RGBA -from gl_utils import draw_circle_filled +from gl_utils import draw_circle_filled_func_builder from methods import denormalize @@ -25,6 +25,7 @@ def __init__(self, g_pool): super().__init__(g_pool) self.order = 0.8 self.pupil_display_list = [] + self._draw_circle_filled = draw_circle_filled_func_builder() def recent_events(self, events): for pt in events.get("gaze", []): @@ -37,7 +38,7 @@ def recent_events(self, events): def gl_display(self): for pt, a in self.pupil_display_list: # This could be faster if there would be a method to also add multiple colors per point - draw_circle_filled( + self._draw_circle_filled( tuple(pt), size=35 / 2, color=RGBA(1.0, 0.2, 0.4, a), diff --git a/pupil_src/shared_modules/gl_utils/__init__.py b/pupil_src/shared_modules/gl_utils/__init__.py index a19a7fbfa3..1edea23251 100644 --- a/pupil_src/shared_modules/gl_utils/__init__.py +++ b/pupil_src/shared_modules/gl_utils/__init__.py @@ -8,7 +8,7 @@ See COPYING and COPYING.LESSER for license details. ---------------------------------------------------------------------------~(*) """ -from .draw import draw_circle_filled +from .draw import draw_circle_filled_func_builder from .trackball import Trackball from .utils import ( _Rectangle, diff --git a/pupil_src/shared_modules/gl_utils/draw.py b/pupil_src/shared_modules/gl_utils/draw.py index 547c0f2c2b..8a751518c6 100644 --- a/pupil_src/shared_modules/gl_utils/draw.py +++ b/pupil_src/shared_modules/gl_utils/draw.py @@ -16,11 +16,40 @@ from pyglui.cygl.utils import RGBA -def draw_circle_filled( - screen_point: T.Tuple[float, float], size: float, color: RGBA, num_points: int = 50 +def draw_circle_filled_func_builder(cache_size: int = 0): + _circle_points_around_zero_f = _circle_points_around_zero + _circle_points_offset_f = _circle_points_offset + + if cache_size > 0: + _circle_points_around_zero_f = functools.lru_cache(cache_size)( + _circle_points_around_zero_f + ) + _circle_points_offset_f = functools.lru_cache(cache_size)( + _circle_points_offset_f + ) + + return functools.partial( + _draw_circle_filled, + _circle_points_around_zero_f=_circle_points_around_zero_f, + _circle_points_offset_f=_circle_points_offset_f, + ) + + +def _draw_circle_filled( + screen_point: T.Tuple[float, float], + size: float, + color: RGBA, + num_points: int = 50, + *, + _circle_points_around_zero_f, + _circle_points_offset_f, ): - points = _circle_points_offset( - screen_point, radius=size, num_points=num_points, flat=False + points = _circle_points_offset_f( + screen_point, + radius=size, + num_points=num_points, + flat=False, + _circle_points_around_zero_f=_circle_points_around_zero_f, ) gl.glColor4f(color.r, color.g, color.b, color.a) gl.glEnableClientState(gl.GL_VERTEX_ARRAY) @@ -28,12 +57,16 @@ def draw_circle_filled( gl.glDrawArrays(gl.GL_POLYGON, 0, points.shape[0]) -@functools.lru_cache(4) # 4 circles needed to draw calibration marker def _circle_points_offset( - offset: T.Tuple[float, float], radius: float, num_points: int, flat: bool = True + offset: T.Tuple[float, float], + radius: float, + num_points: int, + flat: bool = True, + *, + _circle_points_around_zero_f, ) -> np.ndarray: # NOTE: .copy() to avoid modifying the cached result - points = _circle_points_around_zero(radius, num_points).copy() + points = _circle_points_around_zero_f(radius, num_points).copy() points[:, 0] += offset[0] points[:, 1] += offset[1] if flat: @@ -41,7 +74,6 @@ def _circle_points_offset( return points -@functools.lru_cache(4) # 4 circles needed to draw calibration marker def _circle_points_around_zero(radius: float, num_points: int) -> np.ndarray: t = np.linspace(0, 2 * np.pi, num_points, dtype=np.float64) t.shape = -1, 1 From 0f2fd7a98a379b4f4d69eefbad338bc387923614 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 4 May 2021 09:27:04 +0200 Subject: [PATCH 51/65] Replace asyncore with socketserver in network_time_sync.py --- pupil_src/shared_modules/network_time_sync.py | 64 ++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/pupil_src/shared_modules/network_time_sync.py b/pupil_src/shared_modules/network_time_sync.py index 1fdd53c7ed..6a74a542d3 100644 --- a/pupil_src/shared_modules/network_time_sync.py +++ b/pupil_src/shared_modules/network_time_sync.py @@ -9,11 +9,12 @@ ---------------------------------------------------------------------------~(*) """ +import functools from time import sleep from uvc import get_time_monotonic import socket +import socketserver import threading -import asyncore import struct from random import random @@ -36,49 +37,42 @@ """ -class Time_Echo(asyncore.dispatcher_with_send): +class Time_Echo(socketserver.BaseRequestHandler): """ Subclass do not use directly! reply to request with timestamp """ - def __init__(self, sock, time_fn): + def __init__(self, *args, time_fn, **kwargs): self.time_fn = time_fn - asyncore.dispatcher_with_send.__init__(self, sock) + super().__init__(*args, **kwargs) - def handle_read(self): + def handle(self): # expecting `sync` message - data = self.recv(4) - if data: - self.send(struct.pack(" str: + return self.server_address[0] + + @property + def port(self) -> int: + return self.server_address[1] def __del__(self): logger.debug("Server closed") @@ -92,20 +86,15 @@ class Clock_Sync_Master(threading.Thread): def __init__(self, time_fn): threading.Thread.__init__(self) - self.socket_map = {} - self.server = Time_Echo_Server(time_fn, self.socket_map) + self.server = Time_Echo_Server(time_fn=time_fn) self.start() def run(self): - asyncore.loop(use_poll=True, timeout=1) + self.server.serve_forever() def stop(self): - # we dont use server.close() as this raises a bad file decritoor exception in loop - self.server.connected = False - self.server.accepting = False - self.server.del_channel() + self.server.shutdown() self.join() - self.server.socket.close() logger.debug("Server Thread closed") def terminate(self): @@ -221,8 +210,9 @@ def _get_offset(self): server_socket.send(b"sync") message = server_socket.recv(8) t2 = self.get_time() - t1 = struct.unpack(" Date: Tue, 4 May 2021 11:01:28 +0200 Subject: [PATCH 52/65] Handle multiple client requests in Time_Echo handler --- pupil_src/shared_modules/network_time_sync.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pupil_src/shared_modules/network_time_sync.py b/pupil_src/shared_modules/network_time_sync.py index 6a74a542d3..726c951bfc 100644 --- a/pupil_src/shared_modules/network_time_sync.py +++ b/pupil_src/shared_modules/network_time_sync.py @@ -48,10 +48,13 @@ def __init__(self, *args, time_fn, **kwargs): super().__init__(*args, **kwargs) def handle(self): - # expecting `sync` message - data = self.request.recv(4) - if data.decode("utf-8") == "sync": - self.request.send(struct.pack(" Date: Tue, 4 May 2021 11:03:06 +0200 Subject: [PATCH 53/65] Use socket context manager in Clock_Sync_Follower._get_offset --- pupil_src/shared_modules/network_time_sync.py | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/pupil_src/shared_modules/network_time_sync.py b/pupil_src/shared_modules/network_time_sync.py index 726c951bfc..9d1356ed4a 100644 --- a/pupil_src/shared_modules/network_time_sync.py +++ b/pupil_src/shared_modules/network_time_sync.py @@ -203,33 +203,35 @@ def run(self): def _get_offset(self): try: - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.settimeout(1.0) - server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - server_socket.connect((self.host, self.port)) - times = [] - for request in range(60): - t0 = self.get_time() - server_socket.send(b"sync") - message = server_socket.recv(8) - t2 = self.get_time() - if message: - t1 = struct.unpack(" Date: Wed, 5 May 2021 09:29:55 +0200 Subject: [PATCH 54/65] Allow surface detections based on 1 visible marker --- pupil_src/shared_modules/surface_tracker/surface.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pupil_src/shared_modules/surface_tracker/surface.py b/pupil_src/shared_modules/surface_tracker/surface.py index e1d1120359..737ca9a7b0 100644 --- a/pupil_src/shared_modules/surface_tracker/surface.py +++ b/pupil_src/shared_modules/surface_tracker/surface.py @@ -291,11 +291,8 @@ def locate( registered_markers_undist.keys() ) - # If the surface is defined by 2+ markers, we require 2+ markers to be detected. - # If the surface is defined by 1 marker, we require 1 marker to be detected. - if not visible_registered_marker_ids or len( - visible_registered_marker_ids - ) < min(2, len(registered_markers_undist)): + # If no surface marker is detected, return + if not visible_registered_marker_ids: return Surface_Location(detected=False) visible_verts_dist = np.array( From 25c80943a587d61af981be0ff5caeea80cc6fafc Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 5 May 2021 09:30:40 +0200 Subject: [PATCH 55/65] Add registered_marker_uids property to Surface --- pupil_src/shared_modules/surface_tracker/surface.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pupil_src/shared_modules/surface_tracker/surface.py b/pupil_src/shared_modules/surface_tracker/surface.py index 737ca9a7b0..0b0b3fbcc7 100644 --- a/pupil_src/shared_modules/surface_tracker/surface.py +++ b/pupil_src/shared_modules/surface_tracker/surface.py @@ -139,6 +139,10 @@ def property_dict(x: Surface) -> dict: def defined(self): return self.build_up_status >= 1.0 + @property + def registered_marker_uids(self) -> typing.Set[Surface_Marker_UID]: + return set(self._registered_markers_dist.keys()) + @property def registered_markers_dist(self) -> Surface_Marker_UID_To_Aggregate_Mapping: return self._registered_markers_dist From 004d48b8076b5ffac97a9abb474c411ed76bc611 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 5 May 2021 09:31:12 +0200 Subject: [PATCH 56/65] Export number of markers used in surface definition --- pupil_src/shared_modules/surface_tracker/background_tasks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pupil_src/shared_modules/surface_tracker/background_tasks.py b/pupil_src/shared_modules/surface_tracker/background_tasks.py index 1573db71c5..6464a3ab32 100644 --- a/pupil_src/shared_modules/surface_tracker/background_tasks.py +++ b/pupil_src/shared_modules/surface_tracker/background_tasks.py @@ -481,8 +481,10 @@ def _export_surface_positions(self, surface, surface_name): "num_detected_markers", "dist_img_to_surf_trans", "surf_to_dist_img_trans", + "num_definition_markers", ) ) + surface_definition_marker_count = len(surface.registered_marker_uids) for idx, (ts, ref_surf_data) in enumerate( zip(self.world_timestamps, surface.location_cache) ): @@ -501,6 +503,7 @@ def _export_surface_positions(self, surface, surface_name): ref_surf_data.num_detected_markers, ref_surf_data.dist_img_to_surf_trans, ref_surf_data.surf_to_dist_img_trans, + surface_definition_marker_count, ) ) From 137b43a74067154c872c294df698ab40509197bc Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 5 May 2021 09:34:04 +0200 Subject: [PATCH 57/65] Add marker_detections.csv to surface export --- .../surface_tracker/background_tasks.py | 14 ++++++++++++++ .../surface_tracker/surface_tracker_offline.py | 1 + 2 files changed, 15 insertions(+) diff --git a/pupil_src/shared_modules/surface_tracker/background_tasks.py b/pupil_src/shared_modules/surface_tracker/background_tasks.py index 1573db71c5..70585c1427 100644 --- a/pupil_src/shared_modules/surface_tracker/background_tasks.py +++ b/pupil_src/shared_modules/surface_tracker/background_tasks.py @@ -221,6 +221,7 @@ def get_export_proxy( gaze_positions, fixations, camera_model, + marker_cache, mp_context, ): exporter = Exporter( @@ -231,6 +232,7 @@ def get_export_proxy( gaze_positions, fixations, camera_model, + marker_cache, ) proxy = background_helper.IPC_Logging_Task_Proxy( "Offline Surface Tracker Exporter", @@ -250,6 +252,7 @@ def __init__( gaze_positions, fixations, camera_model, + marker_cache, ): self.export_range = export_range self.metrics_dir = os.path.join(export_dir, "surfaces") @@ -260,6 +263,7 @@ def __init__( self.camera_model = camera_model self.gaze_on_surfaces = None self.fixations_on_surfaces = None + self.marker_cache = marker_cache def save_surface_statisics_to_file(self): logger.info("exporting metrics to {}".format(self.metrics_dir)) @@ -277,6 +281,7 @@ def save_surface_statisics_to_file(self): self.fixations_on_surfaces, ) = self._map_gaze_and_fixations() + self._export_marker_detections() self._export_surface_visibility() self._export_surface_gaze_distribution() self._export_surface_events() @@ -338,6 +343,15 @@ def _map_gaze_and_fixations(self): return gaze_on_surface, fixations_on_surface + def _export_marker_detections(self): + file_path = os.path.join(self.metrics_dir, "marker_detections.csv") + with open(file_path, "w", encoding="utf-8", newline="") as csv_file: + csv_writer = csv.writer(csv_file, delimiter=",") + csv_writer.writerow(("world_index", "marker_uid")) + for idx, markers in enumerate(self.marker_cache): + for m in markers: + csv_writer.writerow((idx, m.uid)) + def _export_surface_visibility(self): with open( os.path.join(self.metrics_dir, "surface_visibility.csv"), diff --git a/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py b/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py index 86cdbc4550..6d66a2a52d 100644 --- a/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py +++ b/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py @@ -567,6 +567,7 @@ def on_notify(self, notification): self.g_pool.gaze_positions, self.g_pool.fixations, self.camera_model, + self.marker_cache, mp_context, ) self.export_proxies.add(proxy) From bd375b30c87faa66baf9b12f166d187664434b1c Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 5 May 2021 09:37:58 +0200 Subject: [PATCH 58/65] Remove redundant socket closing call The context manager closes the socket on exit --- pupil_src/shared_modules/network_time_sync.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pupil_src/shared_modules/network_time_sync.py b/pupil_src/shared_modules/network_time_sync.py index 9d1356ed4a..4335df0353 100644 --- a/pupil_src/shared_modules/network_time_sync.py +++ b/pupil_src/shared_modules/network_time_sync.py @@ -217,8 +217,6 @@ def _get_offset(self): t1 = struct.unpack(" Date: Wed, 5 May 2021 09:38:27 +0200 Subject: [PATCH 59/65] Move offset and jitter calculation outside of socket context --- pupil_src/shared_modules/network_time_sync.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/pupil_src/shared_modules/network_time_sync.py b/pupil_src/shared_modules/network_time_sync.py index 4335df0353..0e71e031e7 100644 --- a/pupil_src/shared_modules/network_time_sync.py +++ b/pupil_src/shared_modules/network_time_sync.py @@ -217,19 +217,17 @@ def _get_offset(self): t1 = struct.unpack(" Date: Mon, 10 May 2021 21:48:45 +0200 Subject: [PATCH 60/65] Avoid multiprocessing serialization by saving marker cache to temp file before export --- .../surface_tracker/background_tasks.py | 33 ++++++++++++------- .../surface_tracker_offline.py | 13 +++++++- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/pupil_src/shared_modules/surface_tracker/background_tasks.py b/pupil_src/shared_modules/surface_tracker/background_tasks.py index 70585c1427..0a36f6105f 100644 --- a/pupil_src/shared_modules/surface_tracker/background_tasks.py +++ b/pupil_src/shared_modules/surface_tracker/background_tasks.py @@ -19,6 +19,7 @@ import background_helper import player_methods +import file_methods logger = logging.getLogger(__name__) @@ -221,7 +222,7 @@ def get_export_proxy( gaze_positions, fixations, camera_model, - marker_cache, + marker_cache_path, mp_context, ): exporter = Exporter( @@ -232,7 +233,7 @@ def get_export_proxy( gaze_positions, fixations, camera_model, - marker_cache, + marker_cache_path, ) proxy = background_helper.IPC_Logging_Task_Proxy( "Offline Surface Tracker Exporter", @@ -252,7 +253,7 @@ def __init__( gaze_positions, fixations, camera_model, - marker_cache, + marker_cache_path, ): self.export_range = export_range self.metrics_dir = os.path.join(export_dir, "surfaces") @@ -263,7 +264,7 @@ def __init__( self.camera_model = camera_model self.gaze_on_surfaces = None self.fixations_on_surfaces = None - self.marker_cache = marker_cache + self.marker_cache_path = marker_cache_path def save_surface_statisics_to_file(self): logger.info("exporting metrics to {}".format(self.metrics_dir)) @@ -344,13 +345,23 @@ def _map_gaze_and_fixations(self): return gaze_on_surface, fixations_on_surface def _export_marker_detections(self): - file_path = os.path.join(self.metrics_dir, "marker_detections.csv") - with open(file_path, "w", encoding="utf-8", newline="") as csv_file: - csv_writer = csv.writer(csv_file, delimiter=",") - csv_writer.writerow(("world_index", "marker_uid")) - for idx, markers in enumerate(self.marker_cache): - for m in markers: - csv_writer.writerow((idx, m.uid)) + + # Load the temporary marker cache created by the offline surface tracker + marker_cache = file_methods.Persistent_Dict(self.marker_cache_path) + marker_cache = marker_cache["marker_cache"] + + try: + file_path = os.path.join(self.metrics_dir, "marker_detections.csv") + with open(file_path, "w", encoding="utf-8", newline="") as csv_file: + csv_writer = csv.writer(csv_file, delimiter=",") + csv_writer.writerow(("world_index", "marker_uid")) + for idx, markers in enumerate(marker_cache): + for m in markers: + csv_writer.writerow((idx, m.uid)) + finally: + # Delete the temporary marker cache created by the offline surface tracker + os.remove(self.marker_cache_path) + self.marker_cache_path = None def _export_surface_visibility(self): with open( diff --git a/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py b/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py index 6d66a2a52d..9094085155 100644 --- a/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py +++ b/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py @@ -14,6 +14,7 @@ import multiprocessing import os import platform +import tempfile import time import typing as T @@ -559,6 +560,16 @@ def on_notify(self, notification): break elif notification["subject"] == "should_export": + # Create new marker cache temporary file + # Backgroud exporter is responsible of removing the temporary file when finished + file_handle, marker_cache_path = tempfile.mkstemp() + os.close(file_handle) # https://bugs.python.org/issue42830 + + # Save marker cache into the new temporary file + temp_marker_cache = file_methods.Persistent_Dict(marker_cache_path) + temp_marker_cache["marker_cache"] = self.marker_cache + temp_marker_cache.save() + proxy = background_tasks.get_export_proxy( notification["export_dir"], notification["range"], @@ -567,7 +578,7 @@ def on_notify(self, notification): self.g_pool.gaze_positions, self.g_pool.fixations, self.camera_model, - self.marker_cache, + marker_cache_path, mp_context, ) self.export_proxies.add(proxy) From 647be448fd68d16168231a2d1304c148a19674bc Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 11 May 2021 15:27:39 +0200 Subject: [PATCH 61/65] Treat empty files as non-existent in Persistent_Dict --- pupil_src/shared_modules/file_methods.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/file_methods.py b/pupil_src/shared_modules/file_methods.py index 3afcf892c8..e11a4bca45 100644 --- a/pupil_src/shared_modules/file_methods.py +++ b/pupil_src/shared_modules/file_methods.py @@ -42,7 +42,9 @@ def __init__(self, file_path, *args, **kwargs): super().__init__(*args, **kwargs) self.file_path = os.path.expanduser(file_path) try: - self.update(**load_object(self.file_path, allow_legacy=False)) + if os.path.getsize(file_path) > 0: + # Only try to load object if file is not empty + self.update(**load_object(self.file_path, allow_legacy=False)) except IOError: logger.debug( f"Session settings file '{self.file_path}' not found." From bcc7560ad155a4bfb474665f7c8ca4545b4f7fdc Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 11 May 2021 15:28:13 +0200 Subject: [PATCH 62/65] Correctly deserialize Surface_Marker in background exporter --- .../shared_modules/surface_tracker/background_tasks.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/surface_tracker/background_tasks.py b/pupil_src/shared_modules/surface_tracker/background_tasks.py index 0a36f6105f..25552b0102 100644 --- a/pupil_src/shared_modules/surface_tracker/background_tasks.py +++ b/pupil_src/shared_modules/surface_tracker/background_tasks.py @@ -21,6 +21,9 @@ import player_methods import file_methods +from .surface_marker import Surface_Marker + + logger = logging.getLogger(__name__) @@ -355,8 +358,8 @@ def _export_marker_detections(self): with open(file_path, "w", encoding="utf-8", newline="") as csv_file: csv_writer = csv.writer(csv_file, delimiter=",") csv_writer.writerow(("world_index", "marker_uid")) - for idx, markers in enumerate(marker_cache): - for m in markers: + for idx, serialized_markers in enumerate(marker_cache): + for m in map(Surface_Marker.deserialize, serialized_markers): csv_writer.writerow((idx, m.uid)) finally: # Delete the temporary marker cache created by the offline surface tracker From 3f97de7617ac3abf979fa166bd994ee81da4b935 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Tue, 11 May 2021 17:44:03 +0200 Subject: [PATCH 63/65] Bump pye3d requirement to v0.0.7 pye3d v0.0.7 includes the following improvements - Simplification of `Conic` parameter calculation - [#26](https://github.com/pupil-labs/pye3d-detector/pull/26) - Incremental performance improvements - [#27](https://github.com/pupil-labs/pye3d-detector/pull/27) - Correctly apply corneal-refraction correction to `diameter_3d` result - [#28](https://github.com/pupil-labs/pye3d-detector/pull/28) --- pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py index 469a43bf8d..5e491e7d3f 100644 --- a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py +++ b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) version_installed = getattr(pye3d, "__version__", "0.0.1") -version_supported = "0.0.6" +version_supported = "0.0.7" if version_installed != version_supported: logger.info( diff --git a/requirements.txt b/requirements.txt index 12ea29d8af..a226763c86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ opencv-python==3.* ; platform_system == "Windows" ### pupil-apriltags==1.0.4 pupil-detectors==2.0.* -pye3d==0.0.6 +pye3d==0.0.7 # pupil-labs/PyAV 0.4.6 av @ git+https://github.com/pupil-labs/PyAV@v0.4.6 ; platform_system != "Windows" From 640751271f4bb1095c026c4c3c9114e92713027b Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Tue, 11 May 2021 18:55:25 +0200 Subject: [PATCH 64/65] Add surface marker corner coordinates to export --- .../surface_tracker/background_tasks.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/surface_tracker/background_tasks.py b/pupil_src/shared_modules/surface_tracker/background_tasks.py index 25552b0102..236c7bc3c5 100644 --- a/pupil_src/shared_modules/surface_tracker/background_tasks.py +++ b/pupil_src/shared_modules/surface_tracker/background_tasks.py @@ -357,10 +357,31 @@ def _export_marker_detections(self): file_path = os.path.join(self.metrics_dir, "marker_detections.csv") with open(file_path, "w", encoding="utf-8", newline="") as csv_file: csv_writer = csv.writer(csv_file, delimiter=",") - csv_writer.writerow(("world_index", "marker_uid")) + csv_writer.writerow( + ( + "world_index", + "marker_uid", + "corner_0_x", + "corner_0_y", + "corner_1_x", + "corner_1_y", + "corner_2_x", + "corner_2_y", + "corner_3_x", + "corner_3_y", + ) + ) for idx, serialized_markers in enumerate(marker_cache): for m in map(Surface_Marker.deserialize, serialized_markers): - csv_writer.writerow((idx, m.uid)) + flat_corners = [x for c in m.verts_px for x in c[0]] + assert len(flat_corners) == 8 # sanity check + csv_writer.writerow( + ( + idx, + m.uid, + *flat_corners, + ) + ) finally: # Delete the temporary marker cache created by the offline surface tracker os.remove(self.marker_cache_path) From f0d43c26ba3170e89bb098351c0a22132847fb97 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Wed, 12 May 2021 10:13:52 +0200 Subject: [PATCH 65/65] Attempt to optimize memory usage in surface tracking exporter --- .../surface_tracker/background_tasks.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/surface_tracker/background_tasks.py b/pupil_src/shared_modules/surface_tracker/background_tasks.py index 236c7bc3c5..893a76a80e 100644 --- a/pupil_src/shared_modules/surface_tracker/background_tasks.py +++ b/pupil_src/shared_modules/surface_tracker/background_tasks.py @@ -285,7 +285,6 @@ def save_surface_statisics_to_file(self): self.fixations_on_surfaces, ) = self._map_gaze_and_fixations() - self._export_marker_detections() self._export_surface_visibility() self._export_surface_gaze_distribution() self._export_surface_events() @@ -307,6 +306,17 @@ def save_surface_statisics_to_file(self): "Saved surface gaze and fixation data for '{}'".format(surface.name) ) + # Cleanup surface related data to release memory + self.surfaces = None + self.fixations = None + self.gaze_positions = None + self.gaze_on_surfaces = None + self.fixations_on_surfaces = None + + # Perform marker export *after* surface data is released + # to avoid holding everything in memory all at once. + self._export_marker_detections() + logger.info("Done exporting reference surface data.") return