From 5221fd3109e94ebdbdba6dd9b7b288f89814b7a7 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 13:11:12 +1300 Subject: [PATCH 01/27] Initial UI Implementation Adds UI module, no actual logic implemented yet. --- .gitignore | 1 + EyeTrackApp/config.py | 5 ++ .../settings/general_settings_widget.py | 2 + .../SmartInversionTrackingSettingsModule.py | 63 +++++++++++++++++++ conftest.py | 5 ++ 5 files changed, 76 insertions(+) create mode 100644 EyeTrackApp/settings/modules/SmartInversionTrackingSettingsModule.py diff --git a/.gitignore b/.gitignore index d6ad2dd..c4fbf3d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ EyeTrackApp/IBO_RIGHT.png /IBO_LEFT.png /eyetrack_settings.backup /eyetrack_settings.json +zBuild.bat diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 1d18cd7..90b29e3 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -217,6 +217,11 @@ class EyeTrackSettingsConfig(BaseModel): gui_OutputMultiplier: float = 1 gui_use_module: bool = False + #SmartInversionTracking + gui_smartinversion_enabled: bool = False + gui_smartinversion_select_right: bool = True + gui_smartinversion_thresh: float = 0.5 + class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/settings/general_settings_widget.py b/EyeTrackApp/settings/general_settings_widget.py index 1031cd6..caeab82 100644 --- a/EyeTrackApp/settings/general_settings_widget.py +++ b/EyeTrackApp/settings/general_settings_widget.py @@ -31,6 +31,7 @@ from settings.BaseSettings import BaseSettingsWidget from settings.modules.GeneralSettingsModule import GeneralSettingsModule from settings.modules.OneEuroSettingsModule import OneEuroSettingsModule from settings.modules.OSCSettingsModule import OSCSettingsModule +from settings.modules.SmartInversionTrackingSettingsModule import SmartInversionSettingsModule class SettingsWidget(BaseSettingsWidget): @@ -38,6 +39,7 @@ class SettingsWidget(BaseSettingsWidget): settings_modules = [ GeneralSettingsModule, OneEuroSettingsModule, + SmartInversionSettingsModule, OSCSettingsModule, ] super().__init__(widget_id, main_config, settings_modules) diff --git a/EyeTrackApp/settings/modules/SmartInversionTrackingSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionTrackingSettingsModule.py new file mode 100644 index 0000000..c72ff62 --- /dev/null +++ b/EyeTrackApp/settings/modules/SmartInversionTrackingSettingsModule.py @@ -0,0 +1,63 @@ +from pydantic import AfterValidator +from typing_extensions import Annotated + +from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel +from settings.constants import BACKGROUND_COLOR +import PySimpleGUI as sg + +from settings.modules.CommonFieldValidators import try_convert_to_float + + +class SmartInversionValidationModule(BaseValidationModel): + gui_smartinversion_enabled: bool + gui_smartinversion_select_right: bool + gui_smartinversion_thresh: Annotated[str, AfterValidator(try_convert_to_float)] + + +class SmartInversionSettingsModule(BaseSettingsModule): + def __init__(self, config, widget_id, **kwargs): + super().__init__(config=config, widget_id=widget_id, **kwargs) + self.gui_smartinversion_enabled = f"-gui_smartinversion_enabled{widget_id}-" + self.gui_smartinversion_select_right = f"-gui_smartinversion_select_right{widget_id}-" + self.gui_smartinversion_thresh = f"-gui_smartinversion_thresh{widget_id}-" + + def get_layout(self): + return [ + [ + sg.Text("Smart Inversion Tracking System:", background_color='#242224'), + ], + [ + sg.Checkbox( + "Enable:", + default=self.config.gui_smartinversion_enabled, + key=self.gui_smartinversion_enabled, + background_color="#424042", + tooltip="Enables Smart Inversion Tracking System", + ), + + sg.Text("Max. X-Axis Difference", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_smartinversion_thresh, + key=self.gui_smartinversion_thresh, + size=(0, 10), + tooltip="Sets the maximum allowed difference in eye position (x-axis) to determine if the eyes are inverted or not." + ), + ], + [ + sg.Radio( + "Use Left Eye", + "smartinversion_selectedeye", + background_color="#424042", + tooltip="Uses the left eye as the tracked eye.", + ), + + sg.Radio( + "Use Right Eye", + "smartinversion_selectedeye", + default=self.config.gui_smartinversion_select_right, + key=self.gui_smartinversion_select_right, + background_color="#424042", + tooltip="Uses the right eye as the tracked eye.", + ) + ] + ] \ No newline at end of file diff --git a/conftest.py b/conftest.py index 4a10c85..224e9f5 100644 --- a/conftest.py +++ b/conftest.py @@ -74,6 +74,11 @@ def eyetrack_settings_config(): gui_osc_vrcft_v2=False, gui_vrc_native=False, gui_pupil_dilation=True, + + #Smart Inversion Tracking + gui_smartinversion_enabled=False, + gui_smartinversion_select_right=True, + gui_smartinversion_thresh=0.5, ) From 9e5f9e183ef09f908373c02800b9a87a945e6de3 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 16:58:56 +1300 Subject: [PATCH 02/27] Initial logic implementation --- EyeTrackApp/config.py | 6 +-- EyeTrackApp/osc_calibrate_filter.py | 6 ++- .../settings/general_settings_widget.py | 2 +- ...ule.py => SmartInversionSettingsModule.py} | 13 +++-- EyeTrackApp/utils/smart_inversion.py | 48 +++++++++++++++++++ 5 files changed, 65 insertions(+), 10 deletions(-) rename EyeTrackApp/settings/modules/{SmartInversionTrackingSettingsModule.py => SmartInversionSettingsModule.py} (83%) create mode 100644 EyeTrackApp/utils/smart_inversion.py diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 90b29e3..15ee660 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -218,9 +218,9 @@ class EyeTrackSettingsConfig(BaseModel): gui_use_module: bool = False #SmartInversionTracking - gui_smartinversion_enabled: bool = False - gui_smartinversion_select_right: bool = True - gui_smartinversion_thresh: float = 0.5 + gui_smartinversion_enabled: bool = False + gui_smartinversion_select_right: bool = True + gui_smartinversion_thresh: float = 0.25 class EyeTrackConfig(BaseModel): diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index ff592d9..51a1813 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -29,6 +29,7 @@ import time from enum import IntEnum from utils.misc_utils import PlaySound, SND_FILENAME, SND_ASYNC, resource_path from utils.eye_falloff import velocity_falloff +from utils.smart_inversion import smart_inversion import socket import struct import threading @@ -335,7 +336,10 @@ class cal: var.past_x = out_x_mult var.past_y = out_y_mult - out_x, out_y = velocity_falloff(self, var, out_x, out_y) + if(self.settings.gui_smartinversion_enabled): + out_x, out_y = smart_inversion(self,var, out_x, out_y) + else: + out_x, out_y = velocity_falloff(self, var, out_x, out_y) try: noisy_point = np.array([float(out_x), float(out_y)]) # fliter our values with a One Euro Filter diff --git a/EyeTrackApp/settings/general_settings_widget.py b/EyeTrackApp/settings/general_settings_widget.py index caeab82..e63164e 100644 --- a/EyeTrackApp/settings/general_settings_widget.py +++ b/EyeTrackApp/settings/general_settings_widget.py @@ -31,7 +31,7 @@ from settings.BaseSettings import BaseSettingsWidget from settings.modules.GeneralSettingsModule import GeneralSettingsModule from settings.modules.OneEuroSettingsModule import OneEuroSettingsModule from settings.modules.OSCSettingsModule import OSCSettingsModule -from settings.modules.SmartInversionTrackingSettingsModule import SmartInversionSettingsModule +from settings.modules.SmartInversionSettingsModule import SmartInversionSettingsModule class SettingsWidget(BaseSettingsWidget): diff --git a/EyeTrackApp/settings/modules/SmartInversionTrackingSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py similarity index 83% rename from EyeTrackApp/settings/modules/SmartInversionTrackingSettingsModule.py rename to EyeTrackApp/settings/modules/SmartInversionSettingsModule.py index c72ff62..c263091 100644 --- a/EyeTrackApp/settings/modules/SmartInversionTrackingSettingsModule.py +++ b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py @@ -9,17 +9,19 @@ from settings.modules.CommonFieldValidators import try_convert_to_float class SmartInversionValidationModule(BaseValidationModel): - gui_smartinversion_enabled: bool - gui_smartinversion_select_right: bool - gui_smartinversion_thresh: Annotated[str, AfterValidator(try_convert_to_float)] - + gui_smartinversion_enabled: bool + gui_smartinversion_select_right: bool + gui_smartinversion_thresh: Annotated[float, AfterValidator(try_convert_to_float)] + gui_smartinversion_recessive_difference: Annotated[float, AfterValidator(try_convert_to_float)] class SmartInversionSettingsModule(BaseSettingsModule): def __init__(self, config, widget_id, **kwargs): super().__init__(config=config, widget_id=widget_id, **kwargs) + self.validation_model = SmartInversionValidationModule self.gui_smartinversion_enabled = f"-gui_smartinversion_enabled{widget_id}-" self.gui_smartinversion_select_right = f"-gui_smartinversion_select_right{widget_id}-" self.gui_smartinversion_thresh = f"-gui_smartinversion_thresh{widget_id}-" + def get_layout(self): return [ @@ -34,7 +36,8 @@ class SmartInversionSettingsModule(BaseSettingsModule): background_color="#424042", tooltip="Enables Smart Inversion Tracking System", ), - + ], + [ sg.Text("Max. X-Axis Difference", background_color=BACKGROUND_COLOR), sg.InputText( self.config.gui_smartinversion_thresh, diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py new file mode 100644 index 0000000..235c932 --- /dev/null +++ b/EyeTrackApp/utils/smart_inversion.py @@ -0,0 +1,48 @@ +from eye import EyeId +from utils.misc_utils import clamp + +inverted_frames = int +cleared_frames = int + +def smart_inversion(self, var, out_x, out_y): + + #Updates eye positions with latest + if self.eye_id == EyeId.LEFT: + var.l_eye_x = out_x + var.left_y = out_y + + if self.eye_id == EyeId.RIGHT: + var.r_eye_x = out_x + var.right_y = out_y + + #Checks if eyes are inverted + if (var.l_eye_x > 0 and var.r_eye_x < 0) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): + is_inverted = True + else: + is_inverted = False + + #Determines which eye is being tracked based off selection and sets values accordingly + if self.settings.gui_smartinversion_select_right: + tracked_eye_x = var.r_eye_x + tracked_eye_y = var.right_y + recessive_eye = EyeId.LEFT + else: + tracked_eye_x = var.l_eye_x + tracked_eye_y = var.left_y + recessive_eye = EyeId.RIGHT + + out_x = tracked_eye_x + out_y = tracked_eye_y + + #If eyes are inverted, and eye being processed is recessive, invert x value. + if self.eye_id == recessive_eye: + if is_inverted: + out_x = -tracked_eye_x + else: + out_x = tracked_eye_x + else: + out_x = tracked_eye_x + + out_y = tracked_eye_y + + return out_x, out_y From 8c3d1de4e8441fb389d42364c922393e9ab849b3 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 18:18:50 +1300 Subject: [PATCH 03/27] Include frame delay logic --- EyeTrackApp/config.py | 1 + .../settings/modules/CommonFieldValidators.py | 9 ++++ .../modules/SmartInversionSettingsModule.py | 37 ++++++++++------ EyeTrackApp/utils/smart_inversion.py | 44 +++++++++++++++---- conftest.py | 1 + 5 files changed, 71 insertions(+), 21 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 15ee660..54a1b0f 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -221,6 +221,7 @@ class EyeTrackSettingsConfig(BaseModel): gui_smartinversion_enabled: bool = False gui_smartinversion_select_right: bool = True gui_smartinversion_thresh: float = 0.25 + gui_smartinversion_frame_count: int = 10 class EyeTrackConfig(BaseModel): diff --git a/EyeTrackApp/settings/modules/CommonFieldValidators.py b/EyeTrackApp/settings/modules/CommonFieldValidators.py index b42dab5..c15bdaf 100644 --- a/EyeTrackApp/settings/modules/CommonFieldValidators.py +++ b/EyeTrackApp/settings/modules/CommonFieldValidators.py @@ -32,3 +32,12 @@ def check_is_ip_address(v: str): return v except ValueError: raise ValueError("Please provide a valid IP Address") + +def try_convert_to_int(v: str): + """" + Checks if value provided can be converted to an integer and returns the converted result + """ + try: + return int(v) + except ValueError: + raise ValueError("Please provide a number with no decimal points") \ No newline at end of file diff --git a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py index c263091..1ea8c18 100644 --- a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py +++ b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py @@ -6,13 +6,15 @@ from settings.constants import BACKGROUND_COLOR import PySimpleGUI as sg from settings.modules.CommonFieldValidators import try_convert_to_float +from settings.modules.CommonFieldValidators import try_convert_to_int + class SmartInversionValidationModule(BaseValidationModel): gui_smartinversion_enabled: bool gui_smartinversion_select_right: bool gui_smartinversion_thresh: Annotated[float, AfterValidator(try_convert_to_float)] - gui_smartinversion_recessive_difference: Annotated[float, AfterValidator(try_convert_to_float)] + gui_smartinversion_frame_count: Annotated[int, AfterValidator(try_convert_to_int)] class SmartInversionSettingsModule(BaseSettingsModule): def __init__(self, config, widget_id, **kwargs): @@ -21,6 +23,7 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.gui_smartinversion_enabled = f"-gui_smartinversion_enabled{widget_id}-" self.gui_smartinversion_select_right = f"-gui_smartinversion_select_right{widget_id}-" self.gui_smartinversion_thresh = f"-gui_smartinversion_thresh{widget_id}-" + self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}" def get_layout(self): @@ -36,17 +39,7 @@ class SmartInversionSettingsModule(BaseSettingsModule): background_color="#424042", tooltip="Enables Smart Inversion Tracking System", ), - ], - [ - sg.Text("Max. X-Axis Difference", background_color=BACKGROUND_COLOR), - sg.InputText( - self.config.gui_smartinversion_thresh, - key=self.gui_smartinversion_thresh, - size=(0, 10), - tooltip="Sets the maximum allowed difference in eye position (x-axis) to determine if the eyes are inverted or not." - ), - ], - [ + sg.Radio( "Use Left Eye", "smartinversion_selectedeye", @@ -62,5 +55,23 @@ class SmartInversionSettingsModule(BaseSettingsModule): background_color="#424042", tooltip="Uses the right eye as the tracked eye.", ) - ] + ], + [ + sg.Text("Max. X-Axis Difference", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_smartinversion_thresh, + key=self.gui_smartinversion_thresh, + size=(0, 10), + tooltip="Sets the maximum allowed difference in eye position (x-axis) to determine if the eyes are inverted or not." + ), + ], + [ + sg.Text("Inversion Trigger Frame Count", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_smartinversion_frame_count, + key=self.gui_smartinversion_frame_count, + size=(0, 10), + tooltip="How many frames the inversion conditions must be true (or no longer true) before the inversion state is toggled on or back off." + ), + ], ] \ No newline at end of file diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 235c932..1f343c6 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -1,11 +1,18 @@ from eye import EyeId from utils.misc_utils import clamp -inverted_frames = int -cleared_frames = int - def smart_inversion(self, var, out_x, out_y): + #Checks to see if the class already has frame counts or inversion attributes + if not hasattr(self, "inverted_frame_count"): + self.inverted_frame_count = 0 + + if not hasattr(self, "normal_frame_count"): + self.normal_frame_count = 0 + + if not hasattr(self, "is_inverted"): + self.is_inverted = False + #Updates eye positions with latest if self.eye_id == EyeId.LEFT: var.l_eye_x = out_x @@ -15,11 +22,32 @@ def smart_inversion(self, var, out_x, out_y): var.r_eye_x = out_x var.right_y = out_y - #Checks if eyes are inverted + #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. if (var.l_eye_x > 0 and var.r_eye_x < 0) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): - is_inverted = True - else: - is_inverted = False + self.inverted_frame_count = min(self.inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) + print(f"Inverted frame count: {self.inverted_frame_count}") + + + if self.inverted_frame_count == self.settings.gui_smartinversion_frame_count: + self.is_inverted = True + self.normal_frame_count = 0 + print(f"Inversion Active") + + #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. + elif self.is_inverted and ( + not (var.l_eye_x > 0 and var.r_eye_x < 0) or + abs(var.l_eye_x - var.r_eye_x) <= self.settings.gui_smartinversion_thresh + ): + + self.normal_frame_count = min(self.normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) + print(f"Normal frame count: {self.normal_frame_count}) + + + if self.normal_frame_count == self.settings.gui_smartinversion_frame_count: + self.is_inverted = False + self.inverted_frame_count = 0 + print(f"Inversion Cleared") + #Determines which eye is being tracked based off selection and sets values accordingly if self.settings.gui_smartinversion_select_right: @@ -36,7 +64,7 @@ def smart_inversion(self, var, out_x, out_y): #If eyes are inverted, and eye being processed is recessive, invert x value. if self.eye_id == recessive_eye: - if is_inverted: + if self.is_inverted: out_x = -tracked_eye_x else: out_x = tracked_eye_x diff --git a/conftest.py b/conftest.py index 224e9f5..6fe7217 100644 --- a/conftest.py +++ b/conftest.py @@ -79,6 +79,7 @@ def eyetrack_settings_config(): gui_smartinversion_enabled=False, gui_smartinversion_select_right=True, gui_smartinversion_thresh=0.5, + gui_smartinversion_frame_count=10, ) From 16ad603911cb1e8ac9ae07344e77cc056e5ec4f5 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 18:19:47 +1300 Subject: [PATCH 04/27] Removed excess debug printing --- EyeTrackApp/utils/smart_inversion.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 1f343c6..03364ab 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -25,13 +25,11 @@ def smart_inversion(self, var, out_x, out_y): #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. if (var.l_eye_x > 0 and var.r_eye_x < 0) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): self.inverted_frame_count = min(self.inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) - print(f"Inverted frame count: {self.inverted_frame_count}") - if self.inverted_frame_count == self.settings.gui_smartinversion_frame_count: self.is_inverted = True self.normal_frame_count = 0 - print(f"Inversion Active") + print(f"Inversion Activated") #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. elif self.is_inverted and ( @@ -40,8 +38,6 @@ def smart_inversion(self, var, out_x, out_y): ): self.normal_frame_count = min(self.normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) - print(f"Normal frame count: {self.normal_frame_count}) - if self.normal_frame_count == self.settings.gui_smartinversion_frame_count: self.is_inverted = False From 3265eb9079923de41c70808ff2f174c8f5e7ef98 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 20:35:04 +1300 Subject: [PATCH 05/27] Updates to smoothing and UI features --- EyeTrackApp/config.py | 2 +- .../modules/SmartInversionSettingsModule.py | 26 ++++++- EyeTrackApp/utils/smart_inversion.py | 73 ++++++++++++------- conftest.py | 1 + 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 54a1b0f..448444e 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -222,7 +222,7 @@ class EyeTrackSettingsConfig(BaseModel): gui_smartinversion_select_right: bool = True gui_smartinversion_thresh: float = 0.25 gui_smartinversion_frame_count: int = 10 - + gui_smartinversion_smoothing_rate: float = 0.025 class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py index 1ea8c18..cc980e9 100644 --- a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py +++ b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py @@ -8,13 +8,12 @@ import PySimpleGUI as sg from settings.modules.CommonFieldValidators import try_convert_to_float from settings.modules.CommonFieldValidators import try_convert_to_int - - class SmartInversionValidationModule(BaseValidationModel): gui_smartinversion_enabled: bool gui_smartinversion_select_right: bool gui_smartinversion_thresh: Annotated[float, AfterValidator(try_convert_to_float)] gui_smartinversion_frame_count: Annotated[int, AfterValidator(try_convert_to_int)] + gui_smartinversion_smoothing_rate: Annotated[float, AfterValidator(try_convert_to_float)] class SmartInversionSettingsModule(BaseSettingsModule): def __init__(self, config, widget_id, **kwargs): @@ -24,6 +23,7 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.gui_smartinversion_select_right = f"-gui_smartinversion_select_right{widget_id}-" self.gui_smartinversion_thresh = f"-gui_smartinversion_thresh{widget_id}-" self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}" + self.gui_smartinversion_smoothing_rate =f"-gui_smartinversion_smoothing_rate{widget_id}" def get_layout(self): @@ -62,7 +62,10 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.config.gui_smartinversion_thresh, key=self.gui_smartinversion_thresh, size=(0, 10), - tooltip="Sets the maximum allowed difference in eye position (x-axis) to determine if the eyes are inverted or not." + tooltip=( + "Sets the maximum allowed difference in eye position (x-axis) to determine if the eyes are cross-eyed or not." + "\n Lower value will make cross-eye detection more sensitive." + ) ), ], [ @@ -71,7 +74,22 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.config.gui_smartinversion_frame_count, key=self.gui_smartinversion_frame_count, size=(0, 10), - tooltip="How many frames the inversion conditions must be true (or no longer true) before the inversion state is toggled on or back off." + tooltip=( + "How long it takes to detect you are cross-eyed, or no longer cross-eyed." + "\n Higher number means longer duration before changing in or out of being cross-eyed state." + ) + ), + ], + [ + sg.Text("Smoothing Decay Rate", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_smartinversion_smoothing_rate, + key=self.gui_smartinversion_smoothing_rate, + size=(0, 10), + tooltip=( + "How quickly eye smoothing decays when you enter or leave a cross-eyed state." + "\nHigher number = shorter smoothing duration." + ) ), ], ] \ No newline at end of file diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 03364ab..05c3815 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -3,15 +3,19 @@ from utils.misc_utils import clamp def smart_inversion(self, var, out_x, out_y): - #Checks to see if the class already has frame counts or inversion attributes - if not hasattr(self, "inverted_frame_count"): - self.inverted_frame_count = 0 - - if not hasattr(self, "normal_frame_count"): - self.normal_frame_count = 0 - - if not hasattr(self, "is_inverted"): - self.is_inverted = False + #Checks to see if the class already has frame counts, inversion attributes or smoothing attributes + if not hasattr(self, "smartinversion_inverted_frame_count"): + self.smartinversion_inverted_frame_count = 0 + if not hasattr(self, "smartinversion_normal_frame_count"): + self.smartinversion_normal_frame_count = 0 + if not hasattr(self, "smartinversion_is_inverted"): + self.smartinversion_is_inverted = False + if not hasattr(self, "smartinversion_smoothing_progress"): + self.smartinversion_smoothing_progress = 0 + if not hasattr(self, "smartinversion_smoothed_eye_x"): + self.smartinversion_smoothed_eye_x = 0.0 + if not hasattr(self, "smartinversion_previous_inversion_state"): + self.smartinversion_previous_inversion_state = False #Updates eye positions with latest if self.eye_id == EyeId.LEFT: @@ -24,48 +28,63 @@ def smart_inversion(self, var, out_x, out_y): #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. if (var.l_eye_x > 0 and var.r_eye_x < 0) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): - self.inverted_frame_count = min(self.inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) + self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) - if self.inverted_frame_count == self.settings.gui_smartinversion_frame_count: - self.is_inverted = True - self.normal_frame_count = 0 - print(f"Inversion Activated") + if self.smartinversion_inverted_frame_count == self.settings.gui_smartinversion_frame_count: + if self.smartinversion_previous_inversion_state != self.smartinversion_is_inverted: + self.smartinversion_normal_frame_count = 0 + self.smartinversion_is_inverted = True + print(f"Inversion Activated") #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. - elif self.is_inverted and ( + elif self.smartinversion_is_inverted and ( not (var.l_eye_x > 0 and var.r_eye_x < 0) or abs(var.l_eye_x - var.r_eye_x) <= self.settings.gui_smartinversion_thresh ): - self.normal_frame_count = min(self.normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) + self.smartinversion_normal_frame_count = min(self.smartinversion_normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) - if self.normal_frame_count == self.settings.gui_smartinversion_frame_count: - self.is_inverted = False - self.inverted_frame_count = 0 - print(f"Inversion Cleared") + if self.smartinversion_normal_frame_count == self.settings.gui_smartinversion_frame_count: + if self.smartinversion_previous_inversion_state != self.smartinversion_is_inverted: + self.smartinversion_is_inverted = False + self.smartinversion_inverted_frame_count = 0 + print(f"Inversion Cleared") + #Checks if the inversion state has recently been toggled, and activates smoothing + if self.smartinversion_previous_inversion_state != self.smartinversion_is_inverted: + self.smartinversion_smoothing_progress = 1 + self.smartinversion_previous_inversion_state = self.smartinversion_is_inverted #Determines which eye is being tracked based off selection and sets values accordingly if self.settings.gui_smartinversion_select_right: tracked_eye_x = var.r_eye_x tracked_eye_y = var.right_y recessive_eye = EyeId.LEFT + dominant_eye = EyeId.RIGHT else: tracked_eye_x = var.l_eye_x tracked_eye_y = var.left_y recessive_eye = EyeId.RIGHT + dominant_eye = EyeId.LEFT out_x = tracked_eye_x out_y = tracked_eye_y - #If eyes are inverted, and eye being processed is recessive, invert x value. - if self.eye_id == recessive_eye: - if self.is_inverted: - out_x = -tracked_eye_x + #Logic if smoothing is activated + if self.smartinversion_smoothing_progress > 0: + smartinversion_lerp_factor = (1 - self.smartinversion_smoothing_progress) + + if self.smartinversion_is_inverted and self.eye_id == recessive_eye: + self.smartinversion_smoothed_eye_x += (-tracked_eye_x - self.smartinversion_smoothed_eye_x) * smartinversion_lerp_factor else: - out_x = tracked_eye_x - else: - out_x = tracked_eye_x + self.smartinversion_smoothed_eye_x += (tracked_eye_x - self.smartinversion_smoothed_eye_x) * smartinversion_lerp_factor + + self.smartinversion_smoothing_progress = max(self.smartinversion_smoothing_progress - self.settings.gui_smartinversion_smoothing_rate, 0) + out_x = self.smartinversion_smoothed_eye_x + + #Logic if inversion is active, but smoothing is not active + elif self.smartinversion_is_inverted and self.eye_id == recessive_eye: + out_x = -tracked_eye_x out_y = tracked_eye_y diff --git a/conftest.py b/conftest.py index 6fe7217..f6d8a01 100644 --- a/conftest.py +++ b/conftest.py @@ -80,6 +80,7 @@ def eyetrack_settings_config(): gui_smartinversion_select_right=True, gui_smartinversion_thresh=0.5, gui_smartinversion_frame_count=10, + gui_smartinversion_smoothing_rate=0.025, ) From 1b7e3d88ffb3624e9f1c7e2aadd7e374af93f42c Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 20:48:19 +1300 Subject: [PATCH 06/27] Fix inversion bug --- EyeTrackApp/utils/smart_inversion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 05c3815..76cf237 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -31,9 +31,9 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_inverted_frame_count == self.settings.gui_smartinversion_frame_count: - if self.smartinversion_previous_inversion_state != self.smartinversion_is_inverted: - self.smartinversion_normal_frame_count = 0 + if not self.smartinversion_is_inverted: self.smartinversion_is_inverted = True + self.smartinversion_normal_frame_count = 0 print(f"Inversion Activated") #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. @@ -45,7 +45,7 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_normal_frame_count = min(self.smartinversion_normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_normal_frame_count == self.settings.gui_smartinversion_frame_count: - if self.smartinversion_previous_inversion_state != self.smartinversion_is_inverted: + if self.smartinversion_is_inverted: self.smartinversion_is_inverted = False self.smartinversion_inverted_frame_count = 0 print(f"Inversion Cleared") From 9331a5dca423cf6bd6b02d822ebc9f00691a2846 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 21:40:40 +1300 Subject: [PATCH 07/27] Add minimum inwards threshold --- EyeTrackApp/config.py | 5 +++-- .../modules/SmartInversionSettingsModule.py | 15 +++++++++++++++ EyeTrackApp/utils/smart_inversion.py | 2 +- conftest.py | 3 ++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 448444e..5a0b108 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -220,9 +220,10 @@ class EyeTrackSettingsConfig(BaseModel): #SmartInversionTracking gui_smartinversion_enabled: bool = False gui_smartinversion_select_right: bool = True - gui_smartinversion_thresh: float = 0.25 - gui_smartinversion_frame_count: int = 10 + gui_smartinversion_thresh: float = 0.4 + gui_smartinversion_frame_count: int = 30 gui_smartinversion_smoothing_rate: float = 0.025 + gui_smartinversion_minthresh: float = 0.3 class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py index cc980e9..464c21c 100644 --- a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py +++ b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py @@ -14,6 +14,7 @@ class SmartInversionValidationModule(BaseValidationModel): gui_smartinversion_thresh: Annotated[float, AfterValidator(try_convert_to_float)] gui_smartinversion_frame_count: Annotated[int, AfterValidator(try_convert_to_int)] gui_smartinversion_smoothing_rate: Annotated[float, AfterValidator(try_convert_to_float)] + gui_smartinversion_minthresh: Annotated[float, AfterValidator(try_convert_to_float)] class SmartInversionSettingsModule(BaseSettingsModule): def __init__(self, config, widget_id, **kwargs): @@ -24,6 +25,8 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.gui_smartinversion_thresh = f"-gui_smartinversion_thresh{widget_id}-" self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}" self.gui_smartinversion_smoothing_rate =f"-gui_smartinversion_smoothing_rate{widget_id}" + self.gui_smartinversion_minthresh =f"-gui_smartinversion_minthresh{widget_id}" + def get_layout(self): @@ -68,6 +71,18 @@ class SmartInversionSettingsModule(BaseSettingsModule): ) ), ], + [ + sg.Text("Inwards Look Threshold", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_smartinversion_minthresh, + key=self.gui_smartinversion_minthresh, + size=(0, 10), + tooltip=( + "Sets the minimum distance of looking in that's required before state can chaned to cross-eyed." + "\n Lower value will make cross-eye detection more sensitive." + ) + ), + ], [ sg.Text("Inversion Trigger Frame Count", background_color=BACKGROUND_COLOR), sg.InputText( diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 76cf237..628d39b 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -38,7 +38,7 @@ def smart_inversion(self, var, out_x, out_y): #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. elif self.smartinversion_is_inverted and ( - not (var.l_eye_x > 0 and var.r_eye_x < 0) or + not (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) or abs(var.l_eye_x - var.r_eye_x) <= self.settings.gui_smartinversion_thresh ): diff --git a/conftest.py b/conftest.py index f6d8a01..46210fa 100644 --- a/conftest.py +++ b/conftest.py @@ -78,9 +78,10 @@ def eyetrack_settings_config(): #Smart Inversion Tracking gui_smartinversion_enabled=False, gui_smartinversion_select_right=True, - gui_smartinversion_thresh=0.5, + gui_smartinversion_thresh=0.4, gui_smartinversion_frame_count=10, gui_smartinversion_smoothing_rate=0.025, + gui_smartinversion_minthresh=0.3, ) From 9f2cac900f7971c1cf12b2dca4344d3b84b2c643 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 23:27:07 +1300 Subject: [PATCH 08/27] Initial commit --- EyeTrackApp/config.py | 5 ++ EyeTrackApp/osc_calibrate_filter.py | 12 +++ .../settings/modules/EyeTuneSettingsModule | 77 +++++++++++++++++++ EyeTrackApp/zBuild.bat | 2 + conftest.py | 6 ++ 5 files changed, 102 insertions(+) create mode 100644 EyeTrackApp/settings/modules/EyeTuneSettingsModule create mode 100644 EyeTrackApp/zBuild.bat diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 1d18cd7..846f779 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -217,6 +217,11 @@ class EyeTrackSettingsConfig(BaseModel): gui_OutputMultiplier: float = 1 gui_use_module: bool = False + #EyeTune + gui_eyetune_maxin: float = 1 + gui_eyetune_maxout: float = 1 + gui_eyetune_maxup: float = 1 + gui_eyetune_maxdown: float = 1 class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index ff592d9..5ee56cb 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -38,6 +38,7 @@ import math from utils.calibration_3d import receive_calibration_data, converge_3d from utils.misc_utils import resource_path from pathlib import Path +from utils.misc_utils import clamp tool = Path("Tools") class TimeoutError(RuntimeError): @@ -335,6 +336,17 @@ class cal: var.past_x = out_x_mult var.past_y = out_y_mult + #Clamps the right eye's X values + if self.eye_id == EyeID.Left: + out_x = clamp(out_x, -self.settings.gui_eyetune_maxout, self.settings.gui_eyetune_maxin) + #Clamps the left eye's x values + elif self.eye_id == EyeID.Right: + out_x = clamp(out_x, -self.settings.gui_eyetune_maxin, self.settings.gui_eyetune_maxout) + #Clamps both eye's Y values + out_y = clamp(out_y, -self.settings.gui_eyetune_maxdown, self.settings.gui_eyetune_maxup) + + + out_x, out_y = velocity_falloff(self, var, out_x, out_y) try: diff --git a/EyeTrackApp/settings/modules/EyeTuneSettingsModule b/EyeTrackApp/settings/modules/EyeTuneSettingsModule new file mode 100644 index 0000000..ea8c05a --- /dev/null +++ b/EyeTrackApp/settings/modules/EyeTuneSettingsModule @@ -0,0 +1,77 @@ +from pydantic import AfterValidator +from typing_extensions import Annotated + +from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel +from settings.constants import BACKGROUND_COLOR +import PySimpleGUI as sg + +from settings.modules.CommonFieldValidators import try_convert_to_float +from settings.modules.CommonFieldValidators import try_convert_to_int + +class EyeTuneValidationModule(BaseValidationModel): + gui_eyetune_maxin: Annotated[float, AfterValidator(try_convert_to_float)] + gui_eyetune_maxout: Annotated[float, AfterValidator(try_convert_to_float)] + gui_eyetune_maxup: Annotated[float, AfterValidator(try_convert_to_float)] + gui_eyetune_maxdown: Annotated[float, AfterValidator(try_convert_to_float)] + +class EyeTuneSettingsModule(BaseSettingsModule): + def __init__(self, config, widget_id, **kwargs): + super().__init__(config=config, widget_id=widget_id, **kwargs) + self.validation_model = EyeTuneValidationModule + self.gui_eyetune_maxin = f"-gui_eyetune_maxin{widget_id}-" + self.gui_eyetune_maxout = f"-gui_eyetune_maxout{widget_id}-" + self.gui_eyetune_maxup =f"-gui_eyetune_maxup{widget_id}-" + self.gui_eyetune_maxdown =f"-gui_eyetune_maxdown{widget_id}" + + + + def get_layout(self): + return [ + [ + sg.Text("Eye Tuning (Max Rotation):", background_color='#242224'), + ], + [ + sg.Text("Max. Inwards", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_eyetune_maxin, + key=self.gui_eyetune_maxin, + size=(0, 10), + tooltip=( + "Sets the maximum allowed inwards rotation" + "\nSet between 0 and 1" + ) + ), + sg.Text("Max. Outwards", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_eyetune_maxout, + key=self.gui_eyetune_maxout, + size=(0, 10), + tooltip=( + "Sets the maximum allowed outwards rotation" + "\nSet between 0 and 1" + ) + ), + ], + [ + sg.Text("Max. Upwards", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_eyetune_maxup, + key=self.gui_eyetune_maxup, + size=(0, 10), + tooltip=( + "Sets the maximum allowed upwards rotation" + "\nSet between 0 and 1" + ) + ), + sg.Text("Max. Down", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_eyetune_maxdown, + key=self.gui_eyetune_maxdown, + size=(0, 10), + tooltip=( + "Sets the maximum allowed downwards rotation" + "\nSet between 0 and 1" + ) + ), + ], + ] \ No newline at end of file diff --git a/EyeTrackApp/zBuild.bat b/EyeTrackApp/zBuild.bat new file mode 100644 index 0000000..9ddc2ac --- /dev/null +++ b/EyeTrackApp/zBuild.bat @@ -0,0 +1,2 @@ +poetry run pyinstaller eyetrackapp.spec +cmd /k \ No newline at end of file diff --git a/conftest.py b/conftest.py index 4a10c85..ede52e2 100644 --- a/conftest.py +++ b/conftest.py @@ -74,6 +74,12 @@ def eyetrack_settings_config(): gui_osc_vrcft_v2=False, gui_vrc_native=False, gui_pupil_dilation=True, + + #EyeTune + gui_eyetune_maxin=1, + gui_eyetune_maxout=1, + gui_eyetune_maxup=1, + gui_eyetune_maxdown=1, ) From 1d552371b247f74b5ba912ec01806de0d10f59f9 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 23:35:00 +1300 Subject: [PATCH 09/27] Fixing initial mistakes --- EyeTrackApp/osc_calibrate_filter.py | 4 ++-- EyeTrackApp/settings/general_settings_widget.py | 2 ++ .../{EyeTuneSettingsModule => EyeTuneSettingsModule.py} | 0 3 files changed, 4 insertions(+), 2 deletions(-) rename EyeTrackApp/settings/modules/{EyeTuneSettingsModule => EyeTuneSettingsModule.py} (100%) diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index 5ee56cb..3fc12e0 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -337,10 +337,10 @@ class cal: var.past_y = out_y_mult #Clamps the right eye's X values - if self.eye_id == EyeID.Left: + if self.eye_id == EyeId.LEFT: out_x = clamp(out_x, -self.settings.gui_eyetune_maxout, self.settings.gui_eyetune_maxin) #Clamps the left eye's x values - elif self.eye_id == EyeID.Right: + elif self.eye_id == EyeId.RIGHT: out_x = clamp(out_x, -self.settings.gui_eyetune_maxin, self.settings.gui_eyetune_maxout) #Clamps both eye's Y values out_y = clamp(out_y, -self.settings.gui_eyetune_maxdown, self.settings.gui_eyetune_maxup) diff --git a/EyeTrackApp/settings/general_settings_widget.py b/EyeTrackApp/settings/general_settings_widget.py index 1031cd6..f7fb301 100644 --- a/EyeTrackApp/settings/general_settings_widget.py +++ b/EyeTrackApp/settings/general_settings_widget.py @@ -31,6 +31,7 @@ from settings.BaseSettings import BaseSettingsWidget from settings.modules.GeneralSettingsModule import GeneralSettingsModule from settings.modules.OneEuroSettingsModule import OneEuroSettingsModule from settings.modules.OSCSettingsModule import OSCSettingsModule +from settings.modules.EyeTuneSettingsModule import EyeTuneSettingsModule class SettingsWidget(BaseSettingsWidget): @@ -39,5 +40,6 @@ class SettingsWidget(BaseSettingsWidget): GeneralSettingsModule, OneEuroSettingsModule, OSCSettingsModule, + EyeTuneSettingsModule, ] super().__init__(widget_id, main_config, settings_modules) diff --git a/EyeTrackApp/settings/modules/EyeTuneSettingsModule b/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py similarity index 100% rename from EyeTrackApp/settings/modules/EyeTuneSettingsModule rename to EyeTrackApp/settings/modules/EyeTuneSettingsModule.py From e65631820e34c08b5d08a22076540ea7a026dee0 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 23:36:41 +1300 Subject: [PATCH 10/27] Remove try_convert_to_int Carry over feature from SmartInversion that doesn't exist here woops --- EyeTrackApp/settings/modules/EyeTuneSettingsModule.py | 1 - 1 file changed, 1 deletion(-) diff --git a/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py b/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py index ea8c05a..ae9a9e2 100644 --- a/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py +++ b/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py @@ -6,7 +6,6 @@ from settings.constants import BACKGROUND_COLOR import PySimpleGUI as sg from settings.modules.CommonFieldValidators import try_convert_to_float -from settings.modules.CommonFieldValidators import try_convert_to_int class EyeTuneValidationModule(BaseValidationModel): gui_eyetune_maxin: Annotated[float, AfterValidator(try_convert_to_float)] From 4018879d37945c819bfda424cf7d1c17abd539a4 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 8 Feb 2025 23:52:49 +1300 Subject: [PATCH 11/27] Tidy UI & Move clamp after processing --- EyeTrackApp/osc_calibrate_filter.py | 6 ++---- EyeTrackApp/settings/modules/EyeTuneSettingsModule.py | 10 ++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index 3fc12e0..eabfee8 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -336,6 +336,8 @@ class cal: var.past_x = out_x_mult var.past_y = out_y_mult + out_x, out_y = velocity_falloff(self, var, out_x, out_y) + #Clamps the right eye's X values if self.eye_id == EyeId.LEFT: out_x = clamp(out_x, -self.settings.gui_eyetune_maxout, self.settings.gui_eyetune_maxin) @@ -345,10 +347,6 @@ class cal: #Clamps both eye's Y values out_y = clamp(out_y, -self.settings.gui_eyetune_maxdown, self.settings.gui_eyetune_maxup) - - - out_x, out_y = velocity_falloff(self, var, out_x, out_y) - try: noisy_point = np.array([float(out_x), float(out_y)]) # fliter our values with a One Euro Filter point_hat = self.one_euro_filter(noisy_point) diff --git a/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py b/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py index ae9a9e2..6319a01 100644 --- a/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py +++ b/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py @@ -30,7 +30,7 @@ class EyeTuneSettingsModule(BaseSettingsModule): sg.Text("Eye Tuning (Max Rotation):", background_color='#242224'), ], [ - sg.Text("Max. Inwards", background_color=BACKGROUND_COLOR), + sg.Text("In:", background_color=BACKGROUND_COLOR), sg.InputText( self.config.gui_eyetune_maxin, key=self.gui_eyetune_maxin, @@ -40,7 +40,7 @@ class EyeTuneSettingsModule(BaseSettingsModule): "\nSet between 0 and 1" ) ), - sg.Text("Max. Outwards", background_color=BACKGROUND_COLOR), + sg.Text("Out:", background_color=BACKGROUND_COLOR), sg.InputText( self.config.gui_eyetune_maxout, key=self.gui_eyetune_maxout, @@ -50,9 +50,7 @@ class EyeTuneSettingsModule(BaseSettingsModule): "\nSet between 0 and 1" ) ), - ], - [ - sg.Text("Max. Upwards", background_color=BACKGROUND_COLOR), + sg.Text("Up:", background_color=BACKGROUND_COLOR), sg.InputText( self.config.gui_eyetune_maxup, key=self.gui_eyetune_maxup, @@ -62,7 +60,7 @@ class EyeTuneSettingsModule(BaseSettingsModule): "\nSet between 0 and 1" ) ), - sg.Text("Max. Down", background_color=BACKGROUND_COLOR), + sg.Text("Down:", background_color=BACKGROUND_COLOR), sg.InputText( self.config.gui_eyetune_maxdown, key=self.gui_eyetune_maxdown, From dc59c8dc06eb239188bb3d15d3d7a7195cce3f77 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 9 Feb 2025 02:28:37 +1300 Subject: [PATCH 12/27] Fix min threshold --- EyeTrackApp/utils/smart_inversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 628d39b..3fd4648 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -27,7 +27,7 @@ def smart_inversion(self, var, out_x, out_y): var.right_y = out_y #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. - if (var.l_eye_x > 0 and var.r_eye_x < 0) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): + if (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_inverted_frame_count == self.settings.gui_smartinversion_frame_count: From 8939c020ff5cb352d2e1dddd4f5c2d4a76d16c0c Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 9 Feb 2025 13:17:37 +1300 Subject: [PATCH 13/27] Set eye to center when beginning inversion, remove smoothing --- EyeTrackApp/config.py | 1 + .../modules/SmartInversionSettingsModule.py | 14 ++++++ EyeTrackApp/utils/smart_inversion.py | 50 +++++++++++-------- conftest.py | 1 + 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 42c26f0..33dcbbb 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -229,6 +229,7 @@ class EyeTrackSettingsConfig(BaseModel): gui_smartinversion_frame_count: int = 30 gui_smartinversion_smoothing_rate: float = 0.025 gui_smartinversion_minthresh: float = 0.3 + gui_smartinversion_rotation_clamp: float = 1.0 class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py index 464c21c..9ec9c89 100644 --- a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py +++ b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py @@ -15,6 +15,7 @@ class SmartInversionValidationModule(BaseValidationModel): gui_smartinversion_frame_count: Annotated[int, AfterValidator(try_convert_to_int)] gui_smartinversion_smoothing_rate: Annotated[float, AfterValidator(try_convert_to_float)] gui_smartinversion_minthresh: Annotated[float, AfterValidator(try_convert_to_float)] + gui_smartinversion_rotation_clamp: Annotated[float, AfterValidator(try_convert_to_float)] class SmartInversionSettingsModule(BaseSettingsModule): def __init__(self, config, widget_id, **kwargs): @@ -26,6 +27,7 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}" self.gui_smartinversion_smoothing_rate =f"-gui_smartinversion_smoothing_rate{widget_id}" self.gui_smartinversion_minthresh =f"-gui_smartinversion_minthresh{widget_id}" + self.gui_smartinversion_rotation_clamp =f"-gui_smartinversion_rotation_clamp{widget_id}-" @@ -107,4 +109,16 @@ class SmartInversionSettingsModule(BaseSettingsModule): ) ), ], + [ + sg.Text("Maximum allowed cross-eye", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_smartinversion_rotation_clamp, + key=self.gui_smartinversion_rotation_clamp, + size=(0, 10), + tooltip=( + "Defines the maximum inwards rotation that is output when cross-eyed." + "\n0 = will only look straight ahead \n0.5 = will go a little bit cross-eyed \n1 = maximum hurr durr " + ) + ), + ], ] \ No newline at end of file diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 3fd4648..1879faa 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -26,6 +26,20 @@ def smart_inversion(self, var, out_x, out_y): var.r_eye_x = out_x var.right_y = out_y + #Determines which eye is being tracked based off selection and sets values accordingly + if self.settings.gui_smartinversion_select_right: + tracked_eye_x = var.r_eye_x + tracked_eye_y = var.right_y + recessive_eye = EyeId.LEFT + dominant_eye = EyeId.RIGHT + + else: + tracked_eye_x = var.l_eye_x + tracked_eye_y = var.left_y + recessive_eye = EyeId.RIGHT + dominant_eye = EyeId.LEFT + + #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. if (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) @@ -34,6 +48,7 @@ def smart_inversion(self, var, out_x, out_y): if not self.smartinversion_is_inverted: self.smartinversion_is_inverted = True self.smartinversion_normal_frame_count = 0 + tracked_eye_x = 0 print(f"Inversion Activated") #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. @@ -50,42 +65,33 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_inverted_frame_count = 0 print(f"Inversion Cleared") + out_x = tracked_eye_x + out_y = tracked_eye_y + #Checks if the inversion state has recently been toggled, and activates smoothing if self.smartinversion_previous_inversion_state != self.smartinversion_is_inverted: self.smartinversion_smoothing_progress = 1 self.smartinversion_previous_inversion_state = self.smartinversion_is_inverted - - #Determines which eye is being tracked based off selection and sets values accordingly - if self.settings.gui_smartinversion_select_right: - tracked_eye_x = var.r_eye_x - tracked_eye_y = var.right_y - recessive_eye = EyeId.LEFT - dominant_eye = EyeId.RIGHT - else: - tracked_eye_x = var.l_eye_x - tracked_eye_y = var.left_y - recessive_eye = EyeId.RIGHT - dominant_eye = EyeId.LEFT - - out_x = tracked_eye_x - out_y = tracked_eye_y - #Logic if smoothing is activated + """#Logic if smoothing is activated if self.smartinversion_smoothing_progress > 0: smartinversion_lerp_factor = (1 - self.smartinversion_smoothing_progress) - if self.smartinversion_is_inverted and self.eye_id == recessive_eye: - self.smartinversion_smoothed_eye_x += (-tracked_eye_x - self.smartinversion_smoothed_eye_x) * smartinversion_lerp_factor - else: + if not self.smartinversion_is_inverted and self.eye_id == recessive_eye: self.smartinversion_smoothed_eye_x += (tracked_eye_x - self.smartinversion_smoothed_eye_x) * smartinversion_lerp_factor self.smartinversion_smoothing_progress = max(self.smartinversion_smoothing_progress - self.settings.gui_smartinversion_smoothing_rate, 0) - out_x = self.smartinversion_smoothed_eye_x + out_x = self.smartinversion_smoothed_eye_x""" #Logic if inversion is active, but smoothing is not active - elif self.smartinversion_is_inverted and self.eye_id == recessive_eye: + if self.smartinversion_is_inverted and self.eye_id == recessive_eye: out_x = -tracked_eye_x - out_y = tracked_eye_y + #Limits the maximum allowed inwards rotation if detected as cross-eyed + if self.smartinversion_is_inverted: + if self.eye_id == EyeId.LEFT: + out_x = min(out_x, self.settings.gui_smartinversion_rotation_clamp) + else: + out_x = max(out_x, -self.settings.gui_smartinversion_rotation_clamp) return out_x, out_y diff --git a/conftest.py b/conftest.py index 6eec4f5..d5b17db 100644 --- a/conftest.py +++ b/conftest.py @@ -87,6 +87,7 @@ def eyetrack_settings_config(): gui_smartinversion_frame_count=10, gui_smartinversion_smoothing_rate=0.025, gui_smartinversion_minthresh=0.3, + gui_smartinversion_rotation_clamp=1.0, ) From db1fa320bbaa5b58325f1e40da2d58e26bc6b053 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 9 Feb 2025 19:10:51 +1300 Subject: [PATCH 14/27] Addition of center gaze logic and removal of abs(left - right) check --- EyeTrackApp/config.py | 1 - .../modules/SmartInversionSettingsModule.py | 14 ------ EyeTrackApp/utils/smart_inversion.py | 46 ++++++++++--------- conftest.py | 1 - 4 files changed, 25 insertions(+), 37 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 33dcbbb..4fe0d7f 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -225,7 +225,6 @@ class EyeTrackSettingsConfig(BaseModel): #SmartInversionTracking gui_smartinversion_enabled: bool = False gui_smartinversion_select_right: bool = True - gui_smartinversion_thresh: float = 0.4 gui_smartinversion_frame_count: int = 30 gui_smartinversion_smoothing_rate: float = 0.025 gui_smartinversion_minthresh: float = 0.3 diff --git a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py index 9ec9c89..dbb940c 100644 --- a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py +++ b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py @@ -11,7 +11,6 @@ from settings.modules.CommonFieldValidators import try_convert_to_int class SmartInversionValidationModule(BaseValidationModel): gui_smartinversion_enabled: bool gui_smartinversion_select_right: bool - gui_smartinversion_thresh: Annotated[float, AfterValidator(try_convert_to_float)] gui_smartinversion_frame_count: Annotated[int, AfterValidator(try_convert_to_int)] gui_smartinversion_smoothing_rate: Annotated[float, AfterValidator(try_convert_to_float)] gui_smartinversion_minthresh: Annotated[float, AfterValidator(try_convert_to_float)] @@ -23,7 +22,6 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.validation_model = SmartInversionValidationModule self.gui_smartinversion_enabled = f"-gui_smartinversion_enabled{widget_id}-" self.gui_smartinversion_select_right = f"-gui_smartinversion_select_right{widget_id}-" - self.gui_smartinversion_thresh = f"-gui_smartinversion_thresh{widget_id}-" self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}" self.gui_smartinversion_smoothing_rate =f"-gui_smartinversion_smoothing_rate{widget_id}" self.gui_smartinversion_minthresh =f"-gui_smartinversion_minthresh{widget_id}" @@ -61,18 +59,6 @@ class SmartInversionSettingsModule(BaseSettingsModule): tooltip="Uses the right eye as the tracked eye.", ) ], - [ - sg.Text("Max. X-Axis Difference", background_color=BACKGROUND_COLOR), - sg.InputText( - self.config.gui_smartinversion_thresh, - key=self.gui_smartinversion_thresh, - size=(0, 10), - tooltip=( - "Sets the maximum allowed difference in eye position (x-axis) to determine if the eyes are cross-eyed or not." - "\n Lower value will make cross-eye detection more sensitive." - ) - ), - ], [ sg.Text("Inwards Look Threshold", background_color=BACKGROUND_COLOR), sg.InputText( diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 1879faa..7aedbe0 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -14,8 +14,8 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_smoothing_progress = 0 if not hasattr(self, "smartinversion_smoothed_eye_x"): self.smartinversion_smoothed_eye_x = 0.0 - if not hasattr(self, "smartinversion_previous_inversion_state"): - self.smartinversion_previous_inversion_state = False + if not hasattr(self, "smartinversion_stare_ahead"): + self.smartinversion_stare_ahead = False #Updates eye positions with latest if self.eye_id == EyeId.LEFT: @@ -39,52 +39,56 @@ def smart_inversion(self, var, out_x, out_y): recessive_eye = EyeId.RIGHT dominant_eye = EyeId.LEFT + #Checks if eyes are straight, and then sets eye gaze forward until inversion threshold is met + if (0 < var.l_eye_x <= self.settings.gui_smartinversion_minthresh) and (self.settings.gui_smartinversion_minthresh <= var.r_eye_x < 0): + tracked_eye_x = 0 + if not self.smartinversion_stare_ahead: + self.smartinversion_smoothing_progress = 1 + self.smartinversion_is_inverted = False + self.smartinversion_stare_ahead = True #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. - if (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) and (abs(var.l_eye_x - var.r_eye_x) > self.settings.gui_smartinversion_thresh): + if (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh): self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_inverted_frame_count == self.settings.gui_smartinversion_frame_count: if not self.smartinversion_is_inverted: - self.smartinversion_is_inverted = True + self.smartinversion_smoothing_progress = 1 self.smartinversion_normal_frame_count = 0 + self.smartinversion_stare_ahead = False tracked_eye_x = 0 print(f"Inversion Activated") #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. - elif self.smartinversion_is_inverted and ( - not (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) or - abs(var.l_eye_x - var.r_eye_x) <= self.settings.gui_smartinversion_thresh - ): - + elif self.smartinversion_is_inverted and (not (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh)): self.smartinversion_normal_frame_count = min(self.smartinversion_normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_normal_frame_count == self.settings.gui_smartinversion_frame_count: if self.smartinversion_is_inverted: self.smartinversion_is_inverted = False self.smartinversion_inverted_frame_count = 0 + self.smartinversion_stare_ahead = False print(f"Inversion Cleared") out_x = tracked_eye_x out_y = tracked_eye_y - - #Checks if the inversion state has recently been toggled, and activates smoothing - if self.smartinversion_previous_inversion_state != self.smartinversion_is_inverted: - self.smartinversion_smoothing_progress = 1 - self.smartinversion_previous_inversion_state = self.smartinversion_is_inverted - """#Logic if smoothing is activated + #Logic if smoothing is activated if self.smartinversion_smoothing_progress > 0: - smartinversion_lerp_factor = (1 - self.smartinversion_smoothing_progress) - - if not self.smartinversion_is_inverted and self.eye_id == recessive_eye: - self.smartinversion_smoothed_eye_x += (tracked_eye_x - self.smartinversion_smoothed_eye_x) * smartinversion_lerp_factor + lerp_factor = 0.2 + if self.smartinversion_is_inverted: + if self.eye_id == recessive_eye: + self.smartinversion_smoothed_eye_x += (-tracked_eye_x - self.smartinversion_smoothed_eye_x) * lerp_factor + + else: + self.smartinversion_smoothed_eye_x += (tracked_eye_x - self.smartinversion_smoothed_eye_x) * lerp_factor + self.smartinversion_smoothing_progress = max(self.smartinversion_smoothing_progress - self.settings.gui_smartinversion_smoothing_rate, 0) - out_x = self.smartinversion_smoothed_eye_x""" + out_x = self.smartinversion_smoothed_eye_x #Logic if inversion is active, but smoothing is not active - if self.smartinversion_is_inverted and self.eye_id == recessive_eye: + elif self.smartinversion_is_inverted and self.eye_id == recessive_eye: out_x = -tracked_eye_x #Limits the maximum allowed inwards rotation if detected as cross-eyed diff --git a/conftest.py b/conftest.py index d5b17db..653d186 100644 --- a/conftest.py +++ b/conftest.py @@ -83,7 +83,6 @@ def eyetrack_settings_config(): #Smart Inversion Tracking gui_smartinversion_enabled=False, gui_smartinversion_select_right=True, - gui_smartinversion_thresh=0.4, gui_smartinversion_frame_count=10, gui_smartinversion_smoothing_rate=0.025, gui_smartinversion_minthresh=0.3, From f842bc37a1f18b8a7605db2a7d89122d908def3c Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 9 Feb 2025 20:45:11 +1300 Subject: [PATCH 15/27] Remove EyeTune stuff from SmartInversion & tidy settings module a bit --- EyeTrackApp/config.py | 5 -- EyeTrackApp/osc_calibrate_filter.py | 9 --- .../settings/general_settings_widget.py | 2 - .../settings/modules/EyeTuneSettingsModule.py | 74 ------------------- .../modules/SmartInversionSettingsModule.py | 42 +++++------ EyeTrackApp/utils/smart_inversion.py | 11 ++- conftest.py | 5 -- 7 files changed, 27 insertions(+), 121 deletions(-) delete mode 100644 EyeTrackApp/settings/modules/EyeTuneSettingsModule.py diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 4fe0d7f..6420e3e 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -217,11 +217,6 @@ class EyeTrackSettingsConfig(BaseModel): gui_OutputMultiplier: float = 1 gui_use_module: bool = False - #EyeTune - gui_eyetune_maxin: float = 1 - gui_eyetune_maxout: float = 1 - gui_eyetune_maxup: float = 1 - gui_eyetune_maxdown: float = 1 #SmartInversionTracking gui_smartinversion_enabled: bool = False gui_smartinversion_select_right: bool = True diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index ac904d8..9cd05bc 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -342,15 +342,6 @@ class cal: else: out_x, out_y = velocity_falloff(self, var, out_x, out_y) - #Clamps the right eye's X values - if self.eye_id == EyeId.LEFT: - out_x = clamp(out_x, -self.settings.gui_eyetune_maxout, self.settings.gui_eyetune_maxin) - #Clamps the left eye's x values - elif self.eye_id == EyeId.RIGHT: - out_x = clamp(out_x, -self.settings.gui_eyetune_maxin, self.settings.gui_eyetune_maxout) - #Clamps both eye's Y values - out_y = clamp(out_y, -self.settings.gui_eyetune_maxdown, self.settings.gui_eyetune_maxup) - try: noisy_point = np.array([float(out_x), float(out_y)]) # fliter our values with a One Euro Filter point_hat = self.one_euro_filter(noisy_point) diff --git a/EyeTrackApp/settings/general_settings_widget.py b/EyeTrackApp/settings/general_settings_widget.py index dc2dea2..e63164e 100644 --- a/EyeTrackApp/settings/general_settings_widget.py +++ b/EyeTrackApp/settings/general_settings_widget.py @@ -31,7 +31,6 @@ from settings.BaseSettings import BaseSettingsWidget from settings.modules.GeneralSettingsModule import GeneralSettingsModule from settings.modules.OneEuroSettingsModule import OneEuroSettingsModule from settings.modules.OSCSettingsModule import OSCSettingsModule -from settings.modules.EyeTuneSettingsModule import EyeTuneSettingsModule from settings.modules.SmartInversionSettingsModule import SmartInversionSettingsModule @@ -42,6 +41,5 @@ class SettingsWidget(BaseSettingsWidget): OneEuroSettingsModule, SmartInversionSettingsModule, OSCSettingsModule, - EyeTuneSettingsModule, ] super().__init__(widget_id, main_config, settings_modules) diff --git a/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py b/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py deleted file mode 100644 index 6319a01..0000000 --- a/EyeTrackApp/settings/modules/EyeTuneSettingsModule.py +++ /dev/null @@ -1,74 +0,0 @@ -from pydantic import AfterValidator -from typing_extensions import Annotated - -from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel -from settings.constants import BACKGROUND_COLOR -import PySimpleGUI as sg - -from settings.modules.CommonFieldValidators import try_convert_to_float - -class EyeTuneValidationModule(BaseValidationModel): - gui_eyetune_maxin: Annotated[float, AfterValidator(try_convert_to_float)] - gui_eyetune_maxout: Annotated[float, AfterValidator(try_convert_to_float)] - gui_eyetune_maxup: Annotated[float, AfterValidator(try_convert_to_float)] - gui_eyetune_maxdown: Annotated[float, AfterValidator(try_convert_to_float)] - -class EyeTuneSettingsModule(BaseSettingsModule): - def __init__(self, config, widget_id, **kwargs): - super().__init__(config=config, widget_id=widget_id, **kwargs) - self.validation_model = EyeTuneValidationModule - self.gui_eyetune_maxin = f"-gui_eyetune_maxin{widget_id}-" - self.gui_eyetune_maxout = f"-gui_eyetune_maxout{widget_id}-" - self.gui_eyetune_maxup =f"-gui_eyetune_maxup{widget_id}-" - self.gui_eyetune_maxdown =f"-gui_eyetune_maxdown{widget_id}" - - - - def get_layout(self): - return [ - [ - sg.Text("Eye Tuning (Max Rotation):", background_color='#242224'), - ], - [ - sg.Text("In:", background_color=BACKGROUND_COLOR), - sg.InputText( - self.config.gui_eyetune_maxin, - key=self.gui_eyetune_maxin, - size=(0, 10), - tooltip=( - "Sets the maximum allowed inwards rotation" - "\nSet between 0 and 1" - ) - ), - sg.Text("Out:", background_color=BACKGROUND_COLOR), - sg.InputText( - self.config.gui_eyetune_maxout, - key=self.gui_eyetune_maxout, - size=(0, 10), - tooltip=( - "Sets the maximum allowed outwards rotation" - "\nSet between 0 and 1" - ) - ), - sg.Text("Up:", background_color=BACKGROUND_COLOR), - sg.InputText( - self.config.gui_eyetune_maxup, - key=self.gui_eyetune_maxup, - size=(0, 10), - tooltip=( - "Sets the maximum allowed upwards rotation" - "\nSet between 0 and 1" - ) - ), - sg.Text("Down:", background_color=BACKGROUND_COLOR), - sg.InputText( - self.config.gui_eyetune_maxdown, - key=self.gui_eyetune_maxdown, - size=(0, 10), - tooltip=( - "Sets the maximum allowed downwards rotation" - "\nSet between 0 and 1" - ) - ), - ], - ] \ No newline at end of file diff --git a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py index dbb940c..6cf9547 100644 --- a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py +++ b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py @@ -22,9 +22,9 @@ class SmartInversionSettingsModule(BaseSettingsModule): self.validation_model = SmartInversionValidationModule self.gui_smartinversion_enabled = f"-gui_smartinversion_enabled{widget_id}-" self.gui_smartinversion_select_right = f"-gui_smartinversion_select_right{widget_id}-" - self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}" - self.gui_smartinversion_smoothing_rate =f"-gui_smartinversion_smoothing_rate{widget_id}" - self.gui_smartinversion_minthresh =f"-gui_smartinversion_minthresh{widget_id}" + self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}-" + self.gui_smartinversion_smoothing_rate =f"-gui_smartinversion_smoothing_rate{widget_id}-" + self.gui_smartinversion_minthresh =f"-gui_smartinversion_minthresh{widget_id}-" self.gui_smartinversion_rotation_clamp =f"-gui_smartinversion_rotation_clamp{widget_id}-" @@ -60,51 +60,47 @@ class SmartInversionSettingsModule(BaseSettingsModule): ) ], [ - sg.Text("Inwards Look Threshold", background_color=BACKGROUND_COLOR), + sg.Text("Inwards Look Threshold", background_color=BACKGROUND_COLOR,tooltip= + "Sets the minimum distance of looking in that's required before state can chaned to cross-eyed." + "\n Lower value will make cross-eye detection more sensitive." + ), sg.InputText( self.config.gui_smartinversion_minthresh, key=self.gui_smartinversion_minthresh, size=(0, 10), - tooltip=( - "Sets the minimum distance of looking in that's required before state can chaned to cross-eyed." - "\n Lower value will make cross-eye detection more sensitive." - ) ), ], [ - sg.Text("Inversion Trigger Frame Count", background_color=BACKGROUND_COLOR), + sg.Text("Inversion Trigger Frame Count", background_color=BACKGROUND_COLOR,tooltip= + "How long it takes to detect you are cross-eyed, or no longer cross-eyed." + "\n Higher number means longer duration before changing in or out of being cross-eyed state." + ), sg.InputText( self.config.gui_smartinversion_frame_count, key=self.gui_smartinversion_frame_count, size=(0, 10), - tooltip=( - "How long it takes to detect you are cross-eyed, or no longer cross-eyed." - "\n Higher number means longer duration before changing in or out of being cross-eyed state." - ) ), ], [ - sg.Text("Smoothing Decay Rate", background_color=BACKGROUND_COLOR), + sg.Text("Smoothing Decay Rate", background_color=BACKGROUND_COLOR,tooltip= + "How quickly eye smoothing decays when you enter or leave a cross-eyed state." + "\nHigher number = shorter smoothing duration." + ), sg.InputText( self.config.gui_smartinversion_smoothing_rate, key=self.gui_smartinversion_smoothing_rate, size=(0, 10), - tooltip=( - "How quickly eye smoothing decays when you enter or leave a cross-eyed state." - "\nHigher number = shorter smoothing duration." - ) ), ], [ - sg.Text("Maximum allowed cross-eye", background_color=BACKGROUND_COLOR), + sg.Text("Maximum allowed cross-eye", background_color=BACKGROUND_COLOR,tooltip= + "Defines the maximum inwards rotation that is output when cross-eyed." + "\n0 = will only look straight ahead \n0.5 = will go a little bit cross-eyed \n1 = maximum hurr durr " + ), sg.InputText( self.config.gui_smartinversion_rotation_clamp, key=self.gui_smartinversion_rotation_clamp, size=(0, 10), - tooltip=( - "Defines the maximum inwards rotation that is output when cross-eyed." - "\n0 = will only look straight ahead \n0.5 = will go a little bit cross-eyed \n1 = maximum hurr durr " - ) ), ], ] \ No newline at end of file diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 7aedbe0..bf60b7c 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -41,10 +41,10 @@ def smart_inversion(self, var, out_x, out_y): #Checks if eyes are straight, and then sets eye gaze forward until inversion threshold is met if (0 < var.l_eye_x <= self.settings.gui_smartinversion_minthresh) and (self.settings.gui_smartinversion_minthresh <= var.r_eye_x < 0): - tracked_eye_x = 0 if not self.smartinversion_stare_ahead: self.smartinversion_smoothing_progress = 1 - self.smartinversion_is_inverted = False + self.smartinversion_is_inverted = False + tracked_eye_x = 0 self.smartinversion_stare_ahead = True #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. @@ -55,12 +55,15 @@ def smart_inversion(self, var, out_x, out_y): if not self.smartinversion_is_inverted: self.smartinversion_smoothing_progress = 1 self.smartinversion_normal_frame_count = 0 + self.smartinversion_is_inverted = True self.smartinversion_stare_ahead = False tracked_eye_x = 0 print(f"Inversion Activated") + elif self.smartinversion_inverted_frame_count > 0: + self.smartinversion_inverted_frame_count = 0 #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. - elif self.smartinversion_is_inverted and (not (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh)): + if self.smartinversion_is_inverted and (not (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh)): self.smartinversion_normal_frame_count = min(self.smartinversion_normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_normal_frame_count == self.settings.gui_smartinversion_frame_count: @@ -69,6 +72,8 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_inverted_frame_count = 0 self.smartinversion_stare_ahead = False print(f"Inversion Cleared") + elif self.smartinversion_normal_frame_count > 0: + self.smartinversion_normal_frame_count = 0 out_x = tracked_eye_x out_y = tracked_eye_y diff --git a/conftest.py b/conftest.py index 653d186..dc8f541 100644 --- a/conftest.py +++ b/conftest.py @@ -75,11 +75,6 @@ def eyetrack_settings_config(): gui_vrc_native=False, gui_pupil_dilation=True, - #EyeTune - gui_eyetune_maxin=1, - gui_eyetune_maxout=1, - gui_eyetune_maxup=1, - gui_eyetune_maxdown=1, #Smart Inversion Tracking gui_smartinversion_enabled=False, gui_smartinversion_select_right=True, From fbd6fb212168f2e03076179899045d5c2818eb1a Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 9 Feb 2025 22:54:09 +1300 Subject: [PATCH 16/27] Made boolsfor commonly used checks --- EyeTrackApp/utils/smart_inversion.py | 59 +++++++++++++++++++++------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index bf60b7c..0d2a1f5 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -30,25 +30,53 @@ def smart_inversion(self, var, out_x, out_y): if self.settings.gui_smartinversion_select_right: tracked_eye_x = var.r_eye_x tracked_eye_y = var.right_y - recessive_eye = EyeId.LEFT - dominant_eye = EyeId.RIGHT + is_rec_eye = (self.eye_id == EyeId.LEFT) + is_dom_eye = (self.eye_id == EyeId.RIGHT) else: tracked_eye_x = var.l_eye_x tracked_eye_y = var.left_y - recessive_eye = EyeId.RIGHT - dominant_eye = EyeId.LEFT + is_rec_eye = (self.eye_id == EyeId.RIGHT) + is_dom_eye = (self.eye_id == EyeId.LEFT) + + #Provides some booleans that are a bit easier to use repeatedly + dom_is_inward = ( + (self.settings.gui_smartinversion_select_right and var.r_eye_x < 0) + or + (not self.settings.gui_smartinversion_select_right and var.l_eye_x > 0) + ) + rec_is_inward = ( + (self.settings.gui_smartinversion_select_right and var.l_eye_x > 0) + or + (not self.settings.gui_smartinversion_select_right and var.r_eye_x < 0) + ) + dom_in_inv_range = ( + (self.settings.gui_smartinversion_select_right and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) + or + (not self.settings.gui_smartinversion_select_right and var.l_eye_x > self.settings.gui_smartinversion_minthresh) + ) + rec_in_inv_range = ( + (self.settings.gui_smartinversion_select_right and var.l_eye_x > self.settings.gui_smartinversion_minthresh) + or + (not self.settings.gui_smartinversion_select_right and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) + ) + looking_same_dir = (var.r_eye_x * var.l_eye_x > 0) + x_diff = abs(var.r_eye_x - var.l_eye_x) #Checks if eyes are straight, and then sets eye gaze forward until inversion threshold is met - if (0 < var.l_eye_x <= self.settings.gui_smartinversion_minthresh) and (self.settings.gui_smartinversion_minthresh <= var.r_eye_x < 0): + if dom_is_inward and not rec_in_inv_range and (rec_is_inward or x_diff > 0.4): if not self.smartinversion_stare_ahead: self.smartinversion_smoothing_progress = 1 self.smartinversion_is_inverted = False + print(f"Stare Ahead Activated") + self.smartinversion_stare_ahead = True tracked_eye_x = 0 - self.smartinversion_stare_ahead = True + elif self.smartinversion_stare_ahead: + print(f"Stare Ahead Deactivated") + self.smartinversion_stare_ahead = False #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. - if (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh): + if dom_is_inward and rec_in_inv_range: self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_inverted_frame_count == self.settings.gui_smartinversion_frame_count: @@ -56,22 +84,21 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_smoothing_progress = 1 self.smartinversion_normal_frame_count = 0 self.smartinversion_is_inverted = True - self.smartinversion_stare_ahead = False tracked_eye_x = 0 print(f"Inversion Activated") elif self.smartinversion_inverted_frame_count > 0: self.smartinversion_inverted_frame_count = 0 #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. - if self.smartinversion_is_inverted and (not (var.l_eye_x > self.settings.gui_smartinversion_minthresh and var.r_eye_x < -self.settings.gui_smartinversion_minthresh)): + if self.smartinversion_is_inverted and (not (dom_in_inv_range and rec_in_inv_range)): self.smartinversion_normal_frame_count = min(self.smartinversion_normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) if self.smartinversion_normal_frame_count == self.settings.gui_smartinversion_frame_count: if self.smartinversion_is_inverted: self.smartinversion_is_inverted = False self.smartinversion_inverted_frame_count = 0 - self.smartinversion_stare_ahead = False - print(f"Inversion Cleared") + print(f"Inversion Deactivated") + elif self.smartinversion_normal_frame_count > 0: self.smartinversion_normal_frame_count = 0 @@ -83,7 +110,7 @@ def smart_inversion(self, var, out_x, out_y): lerp_factor = 0.2 if self.smartinversion_is_inverted: - if self.eye_id == recessive_eye: + if is_rec_eye: self.smartinversion_smoothed_eye_x += (-tracked_eye_x - self.smartinversion_smoothed_eye_x) * lerp_factor else: @@ -93,14 +120,16 @@ def smart_inversion(self, var, out_x, out_y): out_x = self.smartinversion_smoothed_eye_x #Logic if inversion is active, but smoothing is not active - elif self.smartinversion_is_inverted and self.eye_id == recessive_eye: + elif self.smartinversion_is_inverted and is_rec_eye: out_x = -tracked_eye_x #Limits the maximum allowed inwards rotation if detected as cross-eyed if self.smartinversion_is_inverted: if self.eye_id == EyeId.LEFT: - out_x = min(out_x, self.settings.gui_smartinversion_rotation_clamp) + clamped_x = min(out_x, self.settings.gui_smartinversion_rotation_clamp) + out_x = clamped_x else: - out_x = max(out_x, -self.settings.gui_smartinversion_rotation_clamp) + clamped_x = max(out_x, -self.settings.gui_smartinversion_rotation_clamp) + out_x = clamped_x return out_x, out_y From b46b25ff70cf497365cf8d4743902ea65ba8bbc7 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 9 Feb 2025 23:30:01 +1300 Subject: [PATCH 17/27] Update smart_inversion.py --- EyeTrackApp/utils/smart_inversion.py | 58 +++++++++++++++++++++------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 0d2a1f5..91bd184 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -8,6 +8,10 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_inverted_frame_count = 0 if not hasattr(self, "smartinversion_normal_frame_count"): self.smartinversion_normal_frame_count = 0 + if not hasattr(self, "smartinversion_stare_valid_frame_count"): + self.smartinversion_stare_valid_frame_count = 0 + if not hasattr(self, "smartinversion_stare_invalid_frame_count"): + self.smartinversion_stare_invalid_frame_count = 0 if not hasattr(self, "smartinversion_is_inverted"): self.smartinversion_is_inverted = False if not hasattr(self, "smartinversion_smoothing_progress"): @@ -17,6 +21,7 @@ def smart_inversion(self, var, out_x, out_y): if not hasattr(self, "smartinversion_stare_ahead"): self.smartinversion_stare_ahead = False +######################################################################################################## #Updates eye positions with latest if self.eye_id == EyeId.LEFT: var.l_eye_x = out_x @@ -26,6 +31,7 @@ def smart_inversion(self, var, out_x, out_y): var.r_eye_x = out_x var.right_y = out_y +######################################################################################################## #Determines which eye is being tracked based off selection and sets values accordingly if self.settings.gui_smartinversion_select_right: tracked_eye_x = var.r_eye_x @@ -39,6 +45,7 @@ def smart_inversion(self, var, out_x, out_y): is_rec_eye = (self.eye_id == EyeId.RIGHT) is_dom_eye = (self.eye_id == EyeId.LEFT) +######################################################################################################## #Provides some booleans that are a bit easier to use repeatedly dom_is_inward = ( (self.settings.gui_smartinversion_select_right and var.r_eye_x < 0) @@ -63,37 +70,58 @@ def smart_inversion(self, var, out_x, out_y): looking_same_dir = (var.r_eye_x * var.l_eye_x > 0) x_diff = abs(var.r_eye_x - var.l_eye_x) +######################################################################################################## #Checks if eyes are straight, and then sets eye gaze forward until inversion threshold is met if dom_is_inward and not rec_in_inv_range and (rec_is_inward or x_diff > 0.4): - if not self.smartinversion_stare_ahead: - self.smartinversion_smoothing_progress = 1 - self.smartinversion_is_inverted = False - print(f"Stare Ahead Activated") - self.smartinversion_stare_ahead = True - tracked_eye_x = 0 - elif self.smartinversion_stare_ahead: - print(f"Stare Ahead Deactivated") - self.smartinversion_stare_ahead = False + self.smartinversion_stare_valid_frame_count = min(self.smartinversion_stare_valid_frame_count + 1, self.settings.gui_smartinversion_frame_count) + self.smartinversion_stare_invalid_frame_count = 0 + if self.smartinversion_stare_valid_frame_count >= self.settings.gui_smartinversion_frame_count: + + if not self.smartinversion_stare_ahead: + self.smartinversion_smoothing_progress = 1 + self.smartinversion_is_inverted = False + print(f"Stare Ahead Activated") + self.smartinversion_stare_ahead = True + tracked_eye_x = 0 + + elif self.smartinversion_stare_valid_frame_count > 0: + self.smartinversion_stare_valid_frame_count = 0 + + if self.smartinversion_stare_ahead and not (dom_is_inward and not rec_in_inv_range and (rec_is_inward or x_diff > 0.4)): + self.smartinversion_stare_invalid_frame_count = min(self.smartinversion_stare_invalid_frame_count + 1, self.settings.gui_smartinversion_frame_count) + tracked_eye_x = 0 + + if self.smartinversion_stare_invalid_frame_count == self.settings.gui_smartinversion_frame_count: + self.smartinversion_stare_ahead = False + print(f"Stare Ahead Deactivated") + + elif self.smartinversion_stare_invalid_frame_count > 0: + self.smartinversion_stare_invalid_frame_count = 0 + +######################################################################################################## #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. if dom_is_inward and rec_in_inv_range: self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) - if self.smartinversion_inverted_frame_count == self.settings.gui_smartinversion_frame_count: + if self.smartinversion_inverted_frame_count >= self.settings.gui_smartinversion_frame_count: if not self.smartinversion_is_inverted: self.smartinversion_smoothing_progress = 1 self.smartinversion_normal_frame_count = 0 self.smartinversion_is_inverted = True + self.smartinversion_stare_ahead = False tracked_eye_x = 0 print(f"Inversion Activated") + elif self.smartinversion_inverted_frame_count > 0: self.smartinversion_inverted_frame_count = 0 +######################################################################################################## #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. - if self.smartinversion_is_inverted and (not (dom_in_inv_range and rec_in_inv_range)): + if self.smartinversion_is_inverted and (not (dom_is_inward and rec_in_inv_range)): self.smartinversion_normal_frame_count = min(self.smartinversion_normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) - if self.smartinversion_normal_frame_count == self.settings.gui_smartinversion_frame_count: + if self.smartinversion_normal_frame_count >= self.settings.gui_smartinversion_frame_count: if self.smartinversion_is_inverted: self.smartinversion_is_inverted = False self.smartinversion_inverted_frame_count = 0 @@ -102,9 +130,12 @@ def smart_inversion(self, var, out_x, out_y): elif self.smartinversion_normal_frame_count > 0: self.smartinversion_normal_frame_count = 0 + +######################################################################################################## out_x = tracked_eye_x out_y = tracked_eye_y - + +######################################################################################################## #Logic if smoothing is activated if self.smartinversion_smoothing_progress > 0: lerp_factor = 0.2 @@ -119,6 +150,7 @@ def smart_inversion(self, var, out_x, out_y): self.smartinversion_smoothing_progress = max(self.smartinversion_smoothing_progress - self.settings.gui_smartinversion_smoothing_rate, 0) out_x = self.smartinversion_smoothed_eye_x +######################################################################################################## #Logic if inversion is active, but smoothing is not active elif self.smartinversion_is_inverted and is_rec_eye: out_x = -tracked_eye_x From 1c8cbff2d858233cbf5d779b232d5381d1f931f2 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Wed, 12 Feb 2025 17:14:50 +1300 Subject: [PATCH 18/27] New global class based SmartInversion system --- EyeTrackApp/eyetrackapp.py | 3 + EyeTrackApp/osc_calibrate_filter.py | 4 +- EyeTrackApp/utils/CycleCounter.py | 30 +++ EyeTrackApp/utils/smart_inversion.py | 355 +++++++++++++++------------ 4 files changed, 230 insertions(+), 162 deletions(-) create mode 100644 EyeTrackApp/utils/CycleCounter.py diff --git a/EyeTrackApp/eyetrackapp.py b/EyeTrackApp/eyetrackapp.py index 630ae02..996e210 100644 --- a/EyeTrackApp/eyetrackapp.py +++ b/EyeTrackApp/eyetrackapp.py @@ -38,6 +38,7 @@ from settings.algo_settings_widget import AlgoSettingsWidget from osc.osc import OSCManager from osc.OSCMessage import OSCMessage from utils.misc_utils import is_nt, resource_path +from utils.smart_inversion import SmartInversion import cv2 import numpy as np import uuid @@ -283,6 +284,7 @@ def main(): config.register_listener_callback(osc_manager.update) config.register_listener_callback(eyes[0].on_config_update) config.register_listener_callback(eyes[1].on_config_update) + config.register_listener_callback(SmartInversion.config_update) osc_manager.register_listeners( config.settings.gui_osc_recenter_address, @@ -300,6 +302,7 @@ def main(): ) osc_manager.start() + SmartInversion.init_config(config) while True: tint = 33 diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index 9cd05bc..a31a640 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -29,7 +29,7 @@ import time from enum import IntEnum from utils.misc_utils import PlaySound, SND_FILENAME, SND_ASYNC, resource_path from utils.eye_falloff import velocity_falloff -from utils.smart_inversion import smart_inversion +from utils.smart_inversion import SmartInversion import socket import struct import threading @@ -338,7 +338,7 @@ class cal: var.past_y = out_y_mult if(self.settings.gui_smartinversion_enabled): - out_x, out_y = smart_inversion(self,var, out_x, out_y) + out_x, out_y = SmartInversion.process(self.eye_id, out_x, out_y) else: out_x, out_y = velocity_falloff(self, var, out_x, out_y) diff --git a/EyeTrackApp/utils/CycleCounter.py b/EyeTrackApp/utils/CycleCounter.py new file mode 100644 index 0000000..2ccc92b --- /dev/null +++ b/EyeTrackApp/utils/CycleCounter.py @@ -0,0 +1,30 @@ +#Counts how many cycles a condition has been true +class CycleCounter: + def __init__ (self, max_count): + self.count = 0 + self.max_count = max_count + + def increase(self): + self.count += 1 + self.count = min(self.count,self.max_count) + + def decrease(self): + self.count -= 1 + self.count = max(self.count,0) + + def reset(self): + self.count = 0 + + def complete(self): + if self.count >= self.max_count: + return True + return False + + def active(self): + if self.count == 0: + return False + else: + return True + + def get_count(self): + return self.count \ No newline at end of file diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index 91bd184..a57807d 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -1,167 +1,202 @@ from eye import EyeId -from utils.misc_utils import clamp +from enum import Enum +from config import EyeTrackConfig +from utils.CycleCounter import * +import threading -def smart_inversion(self, var, out_x, out_y): - - #Checks to see if the class already has frame counts, inversion attributes or smoothing attributes - if not hasattr(self, "smartinversion_inverted_frame_count"): - self.smartinversion_inverted_frame_count = 0 - if not hasattr(self, "smartinversion_normal_frame_count"): - self.smartinversion_normal_frame_count = 0 - if not hasattr(self, "smartinversion_stare_valid_frame_count"): - self.smartinversion_stare_valid_frame_count = 0 - if not hasattr(self, "smartinversion_stare_invalid_frame_count"): - self.smartinversion_stare_invalid_frame_count = 0 - if not hasattr(self, "smartinversion_is_inverted"): - self.smartinversion_is_inverted = False - if not hasattr(self, "smartinversion_smoothing_progress"): - self.smartinversion_smoothing_progress = 0 - if not hasattr(self, "smartinversion_smoothed_eye_x"): - self.smartinversion_smoothed_eye_x = 0.0 - if not hasattr(self, "smartinversion_stare_ahead"): - self.smartinversion_stare_ahead = False - -######################################################################################################## - #Updates eye positions with latest - if self.eye_id == EyeId.LEFT: - var.l_eye_x = out_x - var.left_y = out_y - - if self.eye_id == EyeId.RIGHT: - var.r_eye_x = out_x - var.right_y = out_y - -######################################################################################################## - #Determines which eye is being tracked based off selection and sets values accordingly - if self.settings.gui_smartinversion_select_right: - tracked_eye_x = var.r_eye_x - tracked_eye_y = var.right_y - is_rec_eye = (self.eye_id == EyeId.LEFT) - is_dom_eye = (self.eye_id == EyeId.RIGHT) - - else: - tracked_eye_x = var.l_eye_x - tracked_eye_y = var.left_y - is_rec_eye = (self.eye_id == EyeId.RIGHT) - is_dom_eye = (self.eye_id == EyeId.LEFT) - -######################################################################################################## - #Provides some booleans that are a bit easier to use repeatedly - dom_is_inward = ( - (self.settings.gui_smartinversion_select_right and var.r_eye_x < 0) - or - (not self.settings.gui_smartinversion_select_right and var.l_eye_x > 0) - ) - rec_is_inward = ( - (self.settings.gui_smartinversion_select_right and var.l_eye_x > 0) - or - (not self.settings.gui_smartinversion_select_right and var.r_eye_x < 0) - ) - dom_in_inv_range = ( - (self.settings.gui_smartinversion_select_right and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) - or - (not self.settings.gui_smartinversion_select_right and var.l_eye_x > self.settings.gui_smartinversion_minthresh) - ) - rec_in_inv_range = ( - (self.settings.gui_smartinversion_select_right and var.l_eye_x > self.settings.gui_smartinversion_minthresh) - or - (not self.settings.gui_smartinversion_select_right and var.r_eye_x < -self.settings.gui_smartinversion_minthresh) - ) - looking_same_dir = (var.r_eye_x * var.l_eye_x > 0) - x_diff = abs(var.r_eye_x - var.l_eye_x) - -######################################################################################################## - #Checks if eyes are straight, and then sets eye gaze forward until inversion threshold is met - if dom_is_inward and not rec_in_inv_range and (rec_is_inward or x_diff > 0.4): - self.smartinversion_stare_valid_frame_count = min(self.smartinversion_stare_valid_frame_count + 1, self.settings.gui_smartinversion_frame_count) - self.smartinversion_stare_invalid_frame_count = 0 - - if self.smartinversion_stare_valid_frame_count >= self.settings.gui_smartinversion_frame_count: - - if not self.smartinversion_stare_ahead: - self.smartinversion_smoothing_progress = 1 - self.smartinversion_is_inverted = False - print(f"Stare Ahead Activated") - self.smartinversion_stare_ahead = True - tracked_eye_x = 0 - - elif self.smartinversion_stare_valid_frame_count > 0: - self.smartinversion_stare_valid_frame_count = 0 - - if self.smartinversion_stare_ahead and not (dom_is_inward and not rec_in_inv_range and (rec_is_inward or x_diff > 0.4)): - self.smartinversion_stare_invalid_frame_count = min(self.smartinversion_stare_invalid_frame_count + 1, self.settings.gui_smartinversion_frame_count) - tracked_eye_x = 0 - - if self.smartinversion_stare_invalid_frame_count == self.settings.gui_smartinversion_frame_count: - self.smartinversion_stare_ahead = False - print(f"Stare Ahead Deactivated") - - elif self.smartinversion_stare_invalid_frame_count > 0: - self.smartinversion_stare_invalid_frame_count = 0 - -######################################################################################################## - #Checks if eyes are inverted, and then activates inversion if the conditions have been true for a specified number of frames. - if dom_is_inward and rec_in_inv_range: - self.smartinversion_inverted_frame_count = min(self.smartinversion_inverted_frame_count + 1, self.settings.gui_smartinversion_frame_count) - - if self.smartinversion_inverted_frame_count >= self.settings.gui_smartinversion_frame_count: - if not self.smartinversion_is_inverted: - self.smartinversion_smoothing_progress = 1 - self.smartinversion_normal_frame_count = 0 - self.smartinversion_is_inverted = True - self.smartinversion_stare_ahead = False - tracked_eye_x = 0 - print(f"Inversion Activated") - - elif self.smartinversion_inverted_frame_count > 0: - self.smartinversion_inverted_frame_count = 0 - -######################################################################################################## - #Checks if the eyes are no longer inverted, and then clears inversion if the conditions haven't been true for a specified number of frames. - if self.smartinversion_is_inverted and (not (dom_is_inward and rec_in_inv_range)): - self.smartinversion_normal_frame_count = min(self.smartinversion_normal_frame_count + 1, self.settings.gui_smartinversion_frame_count) - - if self.smartinversion_normal_frame_count >= self.settings.gui_smartinversion_frame_count: - if self.smartinversion_is_inverted: - self.smartinversion_is_inverted = False - self.smartinversion_inverted_frame_count = 0 - print(f"Inversion Deactivated") - - elif self.smartinversion_normal_frame_count > 0: - self.smartinversion_normal_frame_count = 0 - - -######################################################################################################## - out_x = tracked_eye_x - out_y = tracked_eye_y - -######################################################################################################## - #Logic if smoothing is activated - if self.smartinversion_smoothing_progress > 0: - lerp_factor = 0.2 - - if self.smartinversion_is_inverted: - if is_rec_eye: - self.smartinversion_smoothed_eye_x += (-tracked_eye_x - self.smartinversion_smoothed_eye_x) * lerp_factor +class SmartInversion: - else: - self.smartinversion_smoothed_eye_x += (tracked_eye_x - self.smartinversion_smoothed_eye_x) * lerp_factor - - self.smartinversion_smoothing_progress = max(self.smartinversion_smoothing_progress - self.settings.gui_smartinversion_smoothing_rate, 0) - out_x = self.smartinversion_smoothed_eye_x + #Defines our possible tracking states + class States(Enum): + TRACKING = 0 + STARE = 1 + INVERTED = 2 -######################################################################################################## - #Logic if inversion is active, but smoothing is not active - elif self.smartinversion_is_inverted and is_rec_eye: - out_x = -tracked_eye_x + state = States.TRACKING - #Limits the maximum allowed inwards rotation if detected as cross-eyed - if self.smartinversion_is_inverted: - if self.eye_id == EyeId.LEFT: - clamped_x = min(out_x, self.settings.gui_smartinversion_rotation_clamp) - out_x = clamped_x + #Defines our tracked values + thread_lock = threading.Lock() + rx_left_x = 0.0 + rx_left_y = 0.0 + rx_right_x = 0.0 + rx_right_y = 0.0 + dom_eye_x = 0.0 + dom_eye_y = 0.0 + inv_x_thresh = 0.3 + is_r_dom = True + bypass_stare = False + + #Defines variables that require settings + @classmethod + def init_config(cls,config: EyeTrackConfig): + if config.settings.gui_smartinversion_select_right: + cls.dom_eye = EyeId.RIGHT + cls.rec_eye = EyeId.LEFT + cls.is_r_dom = True else: - clamped_x = max(out_x, -self.settings.gui_smartinversion_rotation_clamp) - out_x = clamped_x + cls.dom_eye = EyeId.LEFT + cls.rec_eye = EyeId.RIGHT + cls.is_r_dom = False + + cls.inv_x_thresh = config.settings.gui_smartinversion_minthresh + cls.cycle_counts = config.settings.gui_smartinversion_frame_count + cls.inv_clamp = config.settings.gui_smartinversion_rotation_clamp - return out_x, out_y + cls.inv_counter = CycleCounter(cls.cycle_counts) + cls.stare_counter = CycleCounter(cls.cycle_counts) + cls.track_counter = CycleCounter(cls.cycle_counts) + + #Receives changes in configuration and applies them accordingly + @classmethod + def config_update(cls,data): + print(f"Smart inversion heard that some settings were changed!") + + if "gui_smartinversion_select_right" in data: + cls.dom_eye = EyeId.RIGHT if data["gui_smartinversion_select_right"] else EyeId.LEFT + cls.rec_eye = EyeId.LEFT if data["gui_smartinversion_select_right"] else EyeId.RIGHT + cls.is_r_dom = True if data["gui_smartinversion_select_right"] else False + print(f"Dominant eye changed to {cls.dom_eye.name}") + + if "gui_smartinversion_minthresh" in data: + cls.inv_x_thresh = data["gui_smartinversion_minthresh"] + print(f"Smart inversion transition threshold changed to {cls.inv_x_thresh}") + + if "gui_smartinversion_frame_count" in data: + cls.cycle_counts = data["gui_smartinversion_frame_count"] + print(f"Smart inversion transition condition required cycle count changed to {cls.cycle_counts}") + + if "gui_smartinversion_rotation_clamp" in data: + cls.inv_clamp = data["gui_smartinversion_rotation_clamp"] + print(f"Smart inversion maximum allowed cross-eye changed to {cls.cycle_counts}") + + #Main processing function + @classmethod + def process(cls,eye_id,out_x,out_y): + with cls.thread_lock: + cls.process_tracked_positions(eye_id,out_x,out_y) + cls.check_for_stare() + cls.check_for_inversion() + + if cls.is_stare_mode(): + out_x, out_y = 0, cls.dom_eye_y + + elif not cls.is_dominant_eye(eye_id) and cls.is_inverted_mode(): + out_x, out_y = -cls.dom_eye_x, cls.dom_eye_y + + else: + out_x, out_y = cls.dom_eye_x, cls.dom_eye_y + + if cls.is_inverted_mode(): + if cls.is_processing_right_eye(eye_id): + out_x = max(out_x,-cls.inv_clamp) + else: + out_x = min(out_x,cls.inv_clamp) + + return out_x, out_y + + #Main methods that are called during processing + @classmethod + def process_tracked_positions(cls, eye_id, out_x,out_y): + if cls.is_processing_right_eye(eye_id): + cls.rx_right_x = out_x + cls.rx_right_y = out_y + if cls.is_dominant_eye(eye_id): + cls.dom_eye_x = cls.rx_right_x + cls.dom_eye_y = cls.rx_right_y + else: + cls.rx_left_x = out_x + cls.rx_left_y = out_y + if cls.is_dominant_eye(eye_id): + cls.dom_eye_x = cls.rx_left_x + cls.dom_eye_y = cls.rx_left_y + @classmethod + def check_for_stare(cls): + if cls.dom_is_inward() and cls.rec_is_inward(): + + #Updates the counter for stare activation + if not cls.is_stare_mode() and not cls.stare_counter.complete(): + cls.stare_counter.increase() + + #Sets the state to stare if count completes + elif not cls.is_stare_mode() and cls.stare_counter.complete() and not cls.bypass_stare: + cls.set_state("STARE") + print(f"State is {cls.state.name}") + cls.stare_counter.reset() + + elif cls.is_stare_mode(): + cls.stare_counter.increase() + if cls.stare_counter.complete(): + if cls.bypass_stare: + cls.bypass_stare = False + #print(f"bypass_stare disabled by check_for_stare") + cls.set_state("TRACKING") + print(f"State set to {cls.state.name} by check_for_stare method.") + cls.stare_counter.reset() + cls.inv_counter.reset() + + elif not cls.stare_counter.active(): + cls.stare_counter.reset() + + @classmethod + def check_for_inversion(cls): + if cls.dom_is_inward() and cls.rec_meets_thresh() and cls.is_stare_mode(): + + #Updates the counter for activation + if not cls.is_inverted_mode() and not cls.inv_counter.complete(): + cls.inv_counter.increase() + #print(f"Inversion activation counter is increasing: {cls.inv_counter.get_count()}") + + #Sets the state to inverted if enough cycles have completed + elif not cls.is_inverted_mode() and cls.inv_counter.complete(): + cls.set_state("INVERTED") + print(f"State set to {cls.state.name} by check_for_inv method.") + cls.bypass_stare = True + cls.inv_counter.reset() + + #Begins the counter for deactivation if conditions are not met + elif cls.is_inverted_mode(): + cls.inv_counter.increase() + if cls.inv_counter.complete(): + if cls.bypass_stare: + cls.bypass_stare = False + #print(f"bypass_stare disabled by check_for_inv") + + #Fast resets the counter if conditions are not met, and inversion isn't active. + elif cls.inv_counter.active(): + cls.inv_counter.reset() + print(f"Inversion activation counter was instantly reset as conditions weren't met") + + #Helper Methods + @classmethod + def is_tracking_mode(cls): + return cls.state == cls.States.TRACKING + @classmethod + def is_stare_mode(cls): + return cls.state == cls.States.STARE + @classmethod + def is_inverted_mode(cls): + return cls.state == cls.States.INVERTED + @classmethod + def set_state(cls,new_state: str): + cls.state = cls.States[new_state] + @classmethod + def is_dominant_eye(cls,eye_id): + return eye_id == cls.dom_eye + @classmethod + def is_processing_right_eye(cls,eye_id): + return eye_id == EyeId.RIGHT + @classmethod + def dom_is_inward(cls): + return (cls.is_r_dom and cls.rx_right_x < 0) or (not cls.is_r_dom and cls.rx_left_x > 0) + @classmethod + def rec_is_inward(cls): + return (cls.is_r_dom and cls.rx_left_x > 0) or (not cls.is_r_dom and cls.rx_right_x < 0) + @classmethod + def dom_meets_thresh(cls): + return (cls.is_r_dom and cls.rx_right_x < -cls.inv_x_thresh) or (not cls.is_r_dom and cls.rx_left_x > cls.inv_x_thresh) + @classmethod + def rec_meets_thresh(cls): + return (cls.is_r_dom and cls.rx_left_x > cls.inv_x_thresh) or (not cls.is_r_dom and cls.rx_right_x < -cls.inv_x_thresh) + @classmethod + def x_diff(cls): + return abs(cls.rx_left_x - cls.rx_right_x) From f7d16212f33704778026023a3ad2a4e8f7c6623f Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Wed, 12 Feb 2025 19:13:45 +1300 Subject: [PATCH 19/27] New decrease method --- EyeTrackApp/utils/smart_inversion.py | 123 +++++++++++++++++---------- 1 file changed, 80 insertions(+), 43 deletions(-) diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/smart_inversion.py index a57807d..bccaf00 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/smart_inversion.py @@ -14,14 +14,24 @@ class SmartInversion: state = States.TRACKING - #Defines our tracked values + #Defines our tracked positions thread_lock = threading.Lock() rx_left_x = 0.0 rx_left_y = 0.0 rx_right_x = 0.0 rx_right_y = 0.0 - dom_eye_x = 0.0 - dom_eye_y = 0.0 + rx_dom_eye_x = 0.0 + rx_dom_eye_y = 0.0 + + #Defines our processed positions + tx_left_x = 0.0 + tx_left_y = 0.0 + tx_right_x = 0.0 + tx_right_y = 0.0 + tx_dom_eye_x = 0.0 + tx_dom_eye_y = 0.0 + + #Defines other global variables inv_x_thresh = 0.3 is_r_dom = True bypass_stare = False @@ -42,9 +52,8 @@ class SmartInversion: cls.cycle_counts = config.settings.gui_smartinversion_frame_count cls.inv_clamp = config.settings.gui_smartinversion_rotation_clamp - cls.inv_counter = CycleCounter(cls.cycle_counts) + cls.inv_counter = CycleCounter(cls.cycle_counts*1.25) cls.stare_counter = CycleCounter(cls.cycle_counts) - cls.track_counter = CycleCounter(cls.cycle_counts) #Receives changes in configuration and applies them accordingly @classmethod @@ -73,42 +82,44 @@ class SmartInversion: @classmethod def process(cls,eye_id,out_x,out_y): with cls.thread_lock: - cls.process_tracked_positions(eye_id,out_x,out_y) + cls.store_tracked_positions(eye_id,out_x,out_y) cls.check_for_stare() cls.check_for_inversion() - - if cls.is_stare_mode(): - out_x, out_y = 0, cls.dom_eye_y - - elif not cls.is_dominant_eye(eye_id) and cls.is_inverted_mode(): - out_x, out_y = -cls.dom_eye_x, cls.dom_eye_y - - else: - out_x, out_y = cls.dom_eye_x, cls.dom_eye_y - - if cls.is_inverted_mode(): - if cls.is_processing_right_eye(eye_id): - out_x = max(out_x,-cls.inv_clamp) - else: - out_x = min(out_x,cls.inv_clamp) - + out_x, out_y = cls.update_new_position(eye_id,out_x,out_y) + cls.store_processed_positions(eye_id,out_x,out_y) return out_x, out_y #Main methods that are called during processing @classmethod - def process_tracked_positions(cls, eye_id, out_x,out_y): + def store_tracked_positions(cls, eye_id, out_x,out_y): if cls.is_processing_right_eye(eye_id): cls.rx_right_x = out_x cls.rx_right_y = out_y if cls.is_dominant_eye(eye_id): - cls.dom_eye_x = cls.rx_right_x - cls.dom_eye_y = cls.rx_right_y + cls.rx_dom_eye_x = cls.rx_right_x + cls.rx_dom_eye_y = cls.rx_right_y else: cls.rx_left_x = out_x cls.rx_left_y = out_y if cls.is_dominant_eye(eye_id): - cls.dom_eye_x = cls.rx_left_x - cls.dom_eye_y = cls.rx_left_y + cls.rx_dom_eye_x = cls.rx_left_x + cls.rx_dom_eye_y = cls.rx_left_y + + @classmethod + def store_processed_positions(cls, eye_id, out_x,out_y): + if cls.is_processing_right_eye(eye_id): + cls.tx_right_x = out_x + cls.tx_right_y = out_y + if cls.is_dominant_eye(eye_id): + cls.tx_dom_eye_x = cls.tx_right_x + cls.tx_dom_eye_y = cls.tx_right_y + else: + cls.tx_left_x = out_x + cls.tx_left_y = out_y + if cls.is_dominant_eye(eye_id): + cls.tx_dom_eye_x = cls.tx_left_x + cls.tx_dom_eye_y = cls.tx_left_y + @classmethod def check_for_stare(cls): if cls.dom_is_inward() and cls.rec_is_inward(): @@ -121,21 +132,19 @@ class SmartInversion: elif not cls.is_stare_mode() and cls.stare_counter.complete() and not cls.bypass_stare: cls.set_state("STARE") print(f"State is {cls.state.name}") - cls.stare_counter.reset() elif cls.is_stare_mode(): - cls.stare_counter.increase() - if cls.stare_counter.complete(): + if cls.stare_counter.active(): + cls.stare_counter.decrease() + if not cls.stare_counter.active(): if cls.bypass_stare: cls.bypass_stare = False - #print(f"bypass_stare disabled by check_for_stare") + print(f"bypass_stare disabled by check_for_stare") cls.set_state("TRACKING") - print(f"State set to {cls.state.name} by check_for_stare method.") - cls.stare_counter.reset() - cls.inv_counter.reset() + print(f"State set to {cls.state.name}") elif not cls.stare_counter.active(): - cls.stare_counter.reset() + cls.stare_counter.decrease() @classmethod def check_for_inversion(cls): @@ -149,22 +158,40 @@ class SmartInversion: #Sets the state to inverted if enough cycles have completed elif not cls.is_inverted_mode() and cls.inv_counter.complete(): cls.set_state("INVERTED") - print(f"State set to {cls.state.name} by check_for_inv method.") + print(f"State set to {cls.state.name}") cls.bypass_stare = True - cls.inv_counter.reset() #Begins the counter for deactivation if conditions are not met elif cls.is_inverted_mode(): - cls.inv_counter.increase() - if cls.inv_counter.complete(): + if cls.inv_counter.active(): + cls.inv_counter.decrease() + if not cls.inv_counter.active(): if cls.bypass_stare: cls.bypass_stare = False - #print(f"bypass_stare disabled by check_for_inv") + print(f"bypass_stare disabled by check_for_inv") - #Fast resets the counter if conditions are not met, and inversion isn't active. + #Decreases the counter if conditions are not met, and inversion isn't active. elif cls.inv_counter.active(): - cls.inv_counter.reset() - print(f"Inversion activation counter was instantly reset as conditions weren't met") + cls.inv_counter.decrease() + + @classmethod + def update_new_position(cls,eye_id,out_x,out_y): + if cls.is_stare_mode(): + out_x, out_y = 0, cls.rx_dom_eye_y + + elif not cls.is_dominant_eye(eye_id) and cls.is_inverted_mode(): + out_x, out_y = -cls.rx_dom_eye_x, cls.rx_dom_eye_y + + else: + out_x, out_y = cls.rx_dom_eye_x, cls.rx_dom_eye_y + + if cls.is_inverted_mode(): + if cls.is_processing_right_eye(eye_id): + out_x = max(out_x,-cls.inv_clamp) + else: + out_x = min(out_x,cls.inv_clamp) + + return out_x, out_y #Helper Methods @classmethod @@ -200,3 +227,13 @@ class SmartInversion: @classmethod def x_diff(cls): return abs(cls.rx_left_x - cls.rx_right_x) + + """class Smoothing: + def __init__(self, smoothing_rate=0.05): + self.smoothing_rate = smoothing_rate + self.start_x = 0.0 + self.target_x = 0.0 + self.out + + def process_smoothing(self,out_x,out_y): + out_x = (self.start_x += (self.start_x - self.target_x) * smoothing_rate)""" \ No newline at end of file From 705fe0243c1c33dccb5222a4c0afbb6f68dae796 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 15 Feb 2025 15:48:26 +1300 Subject: [PATCH 20/27] Rename from SmartInversion to MirrorTrack, and inversion bug fixes! Renamed all mentions of Smart Inversion to Mirror Track. Also fixed the stare / inversion rapid cycling, yippee! --- EyeTrackApp/config.py | 16 ++- EyeTrackApp/eyetrackapp.py | 6 +- EyeTrackApp/osc_calibrate_filter.py | 6 +- .../settings/general_settings_widget.py | 4 +- .../modules/MirrorTrackSettingsModule.py | 131 ++++++++++++++++++ .../modules/SmartInversionSettingsModule.py | 106 -------------- EyeTrackApp/utils/CycleCounter.py | 10 +- .../{smart_inversion.py => mirrortrack.py} | 127 +++++++++-------- conftest.py | 16 ++- 9 files changed, 233 insertions(+), 189 deletions(-) create mode 100644 EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py delete mode 100644 EyeTrackApp/settings/modules/SmartInversionSettingsModule.py rename EyeTrackApp/utils/{smart_inversion.py => mirrortrack.py} (60%) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 6420e3e..9519c8f 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -217,13 +217,15 @@ class EyeTrackSettingsConfig(BaseModel): gui_OutputMultiplier: float = 1 gui_use_module: bool = False - #SmartInversionTracking - gui_smartinversion_enabled: bool = False - gui_smartinversion_select_right: bool = True - gui_smartinversion_frame_count: int = 30 - gui_smartinversion_smoothing_rate: float = 0.025 - gui_smartinversion_minthresh: float = 0.3 - gui_smartinversion_rotation_clamp: float = 1.0 + #mirrortrackTracking + gui_mirrortrack_enabled: bool = False + gui_mirrortrack_select_right: bool = True + gui_mirrortrack_cycle_count_inv: int = 20 + gui_mirrortrack_cycle_count_stare: int = 20 + #gui_mirrortrack_smoothing_rate: float = 0.025 + gui_mirrortrack_minthresh: float = 0.3 + gui_mirrortrack_rotation_clamp: float = 0.5 + gui_mirrortrack_enable_inv: bool = True class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/eyetrackapp.py b/EyeTrackApp/eyetrackapp.py index 996e210..407b039 100644 --- a/EyeTrackApp/eyetrackapp.py +++ b/EyeTrackApp/eyetrackapp.py @@ -38,7 +38,7 @@ from settings.algo_settings_widget import AlgoSettingsWidget from osc.osc import OSCManager from osc.OSCMessage import OSCMessage from utils.misc_utils import is_nt, resource_path -from utils.smart_inversion import SmartInversion +from utils.mirrortrack import mirrortrack import cv2 import numpy as np import uuid @@ -284,7 +284,7 @@ def main(): config.register_listener_callback(osc_manager.update) config.register_listener_callback(eyes[0].on_config_update) config.register_listener_callback(eyes[1].on_config_update) - config.register_listener_callback(SmartInversion.config_update) + config.register_listener_callback(mirrortrack.config_update) osc_manager.register_listeners( config.settings.gui_osc_recenter_address, @@ -302,7 +302,7 @@ def main(): ) osc_manager.start() - SmartInversion.init_config(config) + mirrortrack.init_config(config) while True: tint = 33 diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index a31a640..4a85227 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -29,7 +29,7 @@ import time from enum import IntEnum from utils.misc_utils import PlaySound, SND_FILENAME, SND_ASYNC, resource_path from utils.eye_falloff import velocity_falloff -from utils.smart_inversion import SmartInversion +from utils.mirrortrack import mirrortrack import socket import struct import threading @@ -337,8 +337,8 @@ class cal: var.past_x = out_x_mult var.past_y = out_y_mult - if(self.settings.gui_smartinversion_enabled): - out_x, out_y = SmartInversion.process(self.eye_id, out_x, out_y) + if(self.settings.gui_mirrortrack_enabled): + out_x, out_y = mirrortrack.process(self.eye_id, out_x, out_y) else: out_x, out_y = velocity_falloff(self, var, out_x, out_y) diff --git a/EyeTrackApp/settings/general_settings_widget.py b/EyeTrackApp/settings/general_settings_widget.py index e63164e..1f20375 100644 --- a/EyeTrackApp/settings/general_settings_widget.py +++ b/EyeTrackApp/settings/general_settings_widget.py @@ -31,7 +31,7 @@ from settings.BaseSettings import BaseSettingsWidget from settings.modules.GeneralSettingsModule import GeneralSettingsModule from settings.modules.OneEuroSettingsModule import OneEuroSettingsModule from settings.modules.OSCSettingsModule import OSCSettingsModule -from settings.modules.SmartInversionSettingsModule import SmartInversionSettingsModule +from settings.modules.MirrorTrackSettingsModule import MirrorTrackSettingsModule class SettingsWidget(BaseSettingsWidget): @@ -39,7 +39,7 @@ class SettingsWidget(BaseSettingsWidget): settings_modules = [ GeneralSettingsModule, OneEuroSettingsModule, - SmartInversionSettingsModule, + MirrorTrackSettingsModule, OSCSettingsModule, ] super().__init__(widget_id, main_config, settings_modules) diff --git a/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py new file mode 100644 index 0000000..63f31b5 --- /dev/null +++ b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py @@ -0,0 +1,131 @@ +from pydantic import AfterValidator +from typing_extensions import Annotated + +from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel +from settings.constants import BACKGROUND_COLOR +import PySimpleGUI as sg + +from settings.modules.CommonFieldValidators import try_convert_to_float +from settings.modules.CommonFieldValidators import try_convert_to_int + +class MirrorTrackValidationModule(BaseValidationModel): + gui_mirrortrack_enabled: bool + gui_mirrortrack_select_right: bool + gui_mirrortrack_cycle_count_inv: Annotated[int, AfterValidator(try_convert_to_int)] + gui_mirrortrack_cycle_count_stare: Annotated[int, AfterValidator(try_convert_to_int)] + gui_mirrortrack_minthresh: Annotated[float, AfterValidator(try_convert_to_float)] + gui_mirrortrack_rotation_clamp: Annotated[float, AfterValidator(try_convert_to_float)] + gui_mirrortrack_enable_inv: bool + #gui_mirrortrack_smoothing_rate: Annotated[float, AfterValidator(try_convert_to_float)] + +class MirrorTrackSettingsModule(BaseSettingsModule): + def __init__(self, config, widget_id, **kwargs): + super().__init__(config=config, widget_id=widget_id, **kwargs) + self.validation_model = MirrorTrackValidationModule + self.gui_mirrortrack_enabled = f"-gui_mirrortrack_enabled{widget_id}-" + self.gui_mirrortrack_select_right = f"-gui_mirrortrack_select_right{widget_id}-" + self.gui_mirrortrack_cycle_count_inv =f"-gui_mirrortrack_cycle_count_inv{widget_id}-" + self.gui_mirrortrack_cycle_count_stare =f"-gui_mirrortrack_cycle_count_stare{widget_id}-" + self.gui_mirrortrack_minthresh =f"-gui_mirrortrack_minthresh{widget_id}-" + self.gui_mirrortrack_rotation_clamp =f"-gui_mirrortrack_rotation_clamp{widget_id}-" + self.gui_mirrortrack_enable_inv =f"-gui_mirrortrack_enable_inv{widget_id}-" + #self.gui_mirrortrack_smoothing_rate =f"-gui_mirrortrack_smoothing_rate{widget_id}-" + + def get_layout(self): + return [ + [ + sg.Text("MirrorTrack System:", background_color='#242224'), + ], + [ + sg.Checkbox( + "Enable:", + default=self.config.gui_mirrortrack_enabled, + key=self.gui_mirrortrack_enabled, + background_color="#424042", + tooltip="Enables MirrorTrack System", + ), + ], + [ + sg.Radio( + "Use Left Eye", + "mirrortrack_selectedeye", + background_color="#424042", + tooltip="Uses the left eye as the tracked eye.", + ), + + sg.Radio( + "Use Right Eye", + "mirrortrack_selectedeye", + default=self.config.gui_mirrortrack_select_right, + key=self.gui_mirrortrack_select_right, + background_color="#424042", + tooltip="Uses the right eye as the tracked eye.", + ) + ], + [ + sg.Text("Inwards Look Threshold", background_color=BACKGROUND_COLOR,tooltip= + "Sets the minimum distance of looking in that's required before state can chaned to cross-eyed." + "\n Lower value will make cross-eye detection more sensitive." + ), + sg.InputText( + self.config.gui_mirrortrack_minthresh, + key=self.gui_mirrortrack_minthresh, + size=(0, 10), + ), + ], + [ + sg.Text("Transition Cycle Count (Cross Eye)", background_color=BACKGROUND_COLOR,tooltip= + "How long it takes to detect you are cross-eyed, or no longer cross-eyed." + "\n Higher number means longer duration before changing in or out of being cross-eyed state." + ), + sg.InputText( + self.config.gui_mirrortrack_cycle_count_inv, + key=self.gui_mirrortrack_cycle_count_inv, + size=(0, 10), + ), + ], + [ + sg.Text("Transition Cycle Count (Stare Forward)", background_color=BACKGROUND_COLOR,tooltip= + "How long it takes to detect you are staring ahead, or no longer staring ahead." + "\n Higher number means longer duration before changing in or out of being in stare ahead state." + ), + sg.InputText( + self.config.gui_mirrortrack_cycle_count_stare, + key=self.gui_mirrortrack_cycle_count_stare, + size=(0, 10), + ), + ], + #[ + # sg.Text("Smoothing Decay Rate", background_color=BACKGROUND_COLOR,tooltip= + # "How quickly eye smoothing decays when you enter or leave a cross-eyed state." + # "\nHigher number = shorter smoothing duration." + # ), + # sg.InputText( + # self.config.gui_mirrortrack_smoothing_rate, + # key=self.gui_mirrortrack_smoothing_rate, + # size=(0, 10), + # ), + #], + + [ + sg.Checkbox( + "Allow cross-eye:", + default=self.config.gui_mirrortrack_enable_inv, + key=self.gui_mirrortrack_enable_inv, + background_color="#424042", + tooltip="Enables cross-eye functionality", + ), + ], + [ + sg.Text("Maximum allowed cross-eye", background_color=BACKGROUND_COLOR,tooltip= + "Defines the maximum inwards rotation that is output when cross-eyed." + "\n0 = will only look straight ahead \n0.5 = will go a little bit cross-eyed \n1 = maximum hurr durr " + ), + sg.InputText( + self.config.gui_mirrortrack_rotation_clamp, + key=self.gui_mirrortrack_rotation_clamp, + size=(0, 10), + ), + ], + + ] \ No newline at end of file diff --git a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py b/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py deleted file mode 100644 index 6cf9547..0000000 --- a/EyeTrackApp/settings/modules/SmartInversionSettingsModule.py +++ /dev/null @@ -1,106 +0,0 @@ -from pydantic import AfterValidator -from typing_extensions import Annotated - -from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel -from settings.constants import BACKGROUND_COLOR -import PySimpleGUI as sg - -from settings.modules.CommonFieldValidators import try_convert_to_float -from settings.modules.CommonFieldValidators import try_convert_to_int - -class SmartInversionValidationModule(BaseValidationModel): - gui_smartinversion_enabled: bool - gui_smartinversion_select_right: bool - gui_smartinversion_frame_count: Annotated[int, AfterValidator(try_convert_to_int)] - gui_smartinversion_smoothing_rate: Annotated[float, AfterValidator(try_convert_to_float)] - gui_smartinversion_minthresh: Annotated[float, AfterValidator(try_convert_to_float)] - gui_smartinversion_rotation_clamp: Annotated[float, AfterValidator(try_convert_to_float)] - -class SmartInversionSettingsModule(BaseSettingsModule): - def __init__(self, config, widget_id, **kwargs): - super().__init__(config=config, widget_id=widget_id, **kwargs) - self.validation_model = SmartInversionValidationModule - self.gui_smartinversion_enabled = f"-gui_smartinversion_enabled{widget_id}-" - self.gui_smartinversion_select_right = f"-gui_smartinversion_select_right{widget_id}-" - self.gui_smartinversion_frame_count =f"-gui_smartinversion_frame_count{widget_id}-" - self.gui_smartinversion_smoothing_rate =f"-gui_smartinversion_smoothing_rate{widget_id}-" - self.gui_smartinversion_minthresh =f"-gui_smartinversion_minthresh{widget_id}-" - self.gui_smartinversion_rotation_clamp =f"-gui_smartinversion_rotation_clamp{widget_id}-" - - - - def get_layout(self): - return [ - [ - sg.Text("Smart Inversion Tracking System:", background_color='#242224'), - ], - [ - sg.Checkbox( - "Enable:", - default=self.config.gui_smartinversion_enabled, - key=self.gui_smartinversion_enabled, - background_color="#424042", - tooltip="Enables Smart Inversion Tracking System", - ), - - sg.Radio( - "Use Left Eye", - "smartinversion_selectedeye", - background_color="#424042", - tooltip="Uses the left eye as the tracked eye.", - ), - - sg.Radio( - "Use Right Eye", - "smartinversion_selectedeye", - default=self.config.gui_smartinversion_select_right, - key=self.gui_smartinversion_select_right, - background_color="#424042", - tooltip="Uses the right eye as the tracked eye.", - ) - ], - [ - sg.Text("Inwards Look Threshold", background_color=BACKGROUND_COLOR,tooltip= - "Sets the minimum distance of looking in that's required before state can chaned to cross-eyed." - "\n Lower value will make cross-eye detection more sensitive." - ), - sg.InputText( - self.config.gui_smartinversion_minthresh, - key=self.gui_smartinversion_minthresh, - size=(0, 10), - ), - ], - [ - sg.Text("Inversion Trigger Frame Count", background_color=BACKGROUND_COLOR,tooltip= - "How long it takes to detect you are cross-eyed, or no longer cross-eyed." - "\n Higher number means longer duration before changing in or out of being cross-eyed state." - ), - sg.InputText( - self.config.gui_smartinversion_frame_count, - key=self.gui_smartinversion_frame_count, - size=(0, 10), - ), - ], - [ - sg.Text("Smoothing Decay Rate", background_color=BACKGROUND_COLOR,tooltip= - "How quickly eye smoothing decays when you enter or leave a cross-eyed state." - "\nHigher number = shorter smoothing duration." - ), - sg.InputText( - self.config.gui_smartinversion_smoothing_rate, - key=self.gui_smartinversion_smoothing_rate, - size=(0, 10), - ), - ], - [ - sg.Text("Maximum allowed cross-eye", background_color=BACKGROUND_COLOR,tooltip= - "Defines the maximum inwards rotation that is output when cross-eyed." - "\n0 = will only look straight ahead \n0.5 = will go a little bit cross-eyed \n1 = maximum hurr durr " - ), - sg.InputText( - self.config.gui_smartinversion_rotation_clamp, - key=self.gui_smartinversion_rotation_clamp, - size=(0, 10), - ), - ], - ] \ No newline at end of file diff --git a/EyeTrackApp/utils/CycleCounter.py b/EyeTrackApp/utils/CycleCounter.py index 2ccc92b..7fad7a5 100644 --- a/EyeTrackApp/utils/CycleCounter.py +++ b/EyeTrackApp/utils/CycleCounter.py @@ -15,7 +15,7 @@ class CycleCounter: def reset(self): self.count = 0 - def complete(self): + def is_complete(self): if self.count >= self.max_count: return True return False @@ -27,4 +27,10 @@ class CycleCounter: return True def get_count(self): - return self.count \ No newline at end of file + return self.count + + def update(self,max_count): + self.max_count = max_count + + def force_complete(self): + self.count = self.max_count \ No newline at end of file diff --git a/EyeTrackApp/utils/smart_inversion.py b/EyeTrackApp/utils/mirrortrack.py similarity index 60% rename from EyeTrackApp/utils/smart_inversion.py rename to EyeTrackApp/utils/mirrortrack.py index bccaf00..632edc1 100644 --- a/EyeTrackApp/utils/smart_inversion.py +++ b/EyeTrackApp/utils/mirrortrack.py @@ -4,7 +4,7 @@ from config import EyeTrackConfig from utils.CycleCounter import * import threading -class SmartInversion: +class mirrortrack: #Defines our possible tracking states class States(Enum): @@ -15,7 +15,6 @@ class SmartInversion: state = States.TRACKING #Defines our tracked positions - thread_lock = threading.Lock() rx_left_x = 0.0 rx_left_y = 0.0 rx_right_x = 0.0 @@ -39,7 +38,7 @@ class SmartInversion: #Defines variables that require settings @classmethod def init_config(cls,config: EyeTrackConfig): - if config.settings.gui_smartinversion_select_right: + if config.settings.gui_mirrortrack_select_right: cls.dom_eye = EyeId.RIGHT cls.rec_eye = EyeId.LEFT cls.is_r_dom = True @@ -48,35 +47,50 @@ class SmartInversion: cls.rec_eye = EyeId.RIGHT cls.is_r_dom = False - cls.inv_x_thresh = config.settings.gui_smartinversion_minthresh - cls.cycle_counts = config.settings.gui_smartinversion_frame_count - cls.inv_clamp = config.settings.gui_smartinversion_rotation_clamp + #Inversion related + cls.inv_is_enabled = config.settings.gui_mirrortrack_enable_inv + cls.inv_x_thresh = config.settings.gui_mirrortrack_minthresh + cls.inv_clamp = config.settings.gui_mirrortrack_rotation_clamp - cls.inv_counter = CycleCounter(cls.cycle_counts*1.25) - cls.stare_counter = CycleCounter(cls.cycle_counts) + cls.cyc_counts_inv = config.settings.gui_mirrortrack_cycle_count_inv + cls.cyc_counts_stare = config.settings.gui_mirrortrack_cycle_count_stare + + cls.cyc_counter_inv = CycleCounter(cls.cyc_counts_inv) + cls.cyc_counter_stare = CycleCounter(cls.cyc_counts_stare) + + cls.thread_lock = threading.Lock() #Receives changes in configuration and applies them accordingly @classmethod def config_update(cls,data): - print(f"Smart inversion heard that some settings were changed!") - if "gui_smartinversion_select_right" in data: - cls.dom_eye = EyeId.RIGHT if data["gui_smartinversion_select_right"] else EyeId.LEFT - cls.rec_eye = EyeId.LEFT if data["gui_smartinversion_select_right"] else EyeId.RIGHT - cls.is_r_dom = True if data["gui_smartinversion_select_right"] else False + if "gui_mirrortrack_select_right" in data: + cls.dom_eye = EyeId.RIGHT if data["gui_mirrortrack_select_right"] else EyeId.LEFT + cls.rec_eye = EyeId.LEFT if data["gui_mirrortrack_select_right"] else EyeId.RIGHT + cls.is_r_dom = True if data["gui_mirrortrack_select_right"] else False print(f"Dominant eye changed to {cls.dom_eye.name}") - if "gui_smartinversion_minthresh" in data: - cls.inv_x_thresh = data["gui_smartinversion_minthresh"] - print(f"Smart inversion transition threshold changed to {cls.inv_x_thresh}") + if "gui_mirrortrack_minthresh" in data: + cls.inv_x_thresh = data["gui_mirrortrack_minthresh"] + print(f"MirrorTrack transition threshold changed to {cls.inv_x_thresh}") - if "gui_smartinversion_frame_count" in data: - cls.cycle_counts = data["gui_smartinversion_frame_count"] - print(f"Smart inversion transition condition required cycle count changed to {cls.cycle_counts}") + if "gui_mirrortrack_cycle_count_inv" in data: + cls.cyc_counts_stare = data["gui_mirrortrack_cycle_count_inv"] + cls.cyc_counter_stare.update(cls.cyc_counts_stare) + print(f"MirrorTrack inversion transition condition required cycle count changed to {cls.cyc_counts_inv}") - if "gui_smartinversion_rotation_clamp" in data: - cls.inv_clamp = data["gui_smartinversion_rotation_clamp"] - print(f"Smart inversion maximum allowed cross-eye changed to {cls.cycle_counts}") + if "gui_mirrortrack_cycle_count_stare" in data: + cls.cyc_counts_inv = data["gui_mirrortrack_cycle_count_stare"] + cls.cyc_counter_stare.update(cls.cyc_counts_inv) + print(f"MirrorTrack stare transition condition required cycle count changed to {cls.cyc_counts_stare}") + + if "gui_mirrortrack_rotation_clamp" in data: + cls.inv_clamp = data["gui_mirrortrack_rotation_clamp"] + print(f"MirrorTrack maximum allowed cross-eye changed to {cls.inv_clamp}") + + if "gui_mirrortrack_enable_inv" in data: + cls.inv_is_enabled = data["gui_mirrortrack_enable_inv"] + print(f"MirrorTrack allow cross-eye is set to {cls.inv_is_enabled}") #Main processing function @classmethod @@ -84,7 +98,10 @@ class SmartInversion: with cls.thread_lock: cls.store_tracked_positions(eye_id,out_x,out_y) cls.check_for_stare() - cls.check_for_inversion() + + if cls.inv_is_enabled: + cls.check_for_inversion() + out_x, out_y = cls.update_new_position(eye_id,out_x,out_y) cls.store_processed_positions(eye_id,out_x,out_y) return out_x, out_y @@ -125,54 +142,45 @@ class SmartInversion: if cls.dom_is_inward() and cls.rec_is_inward(): #Updates the counter for stare activation - if not cls.is_stare_mode() and not cls.stare_counter.complete(): - cls.stare_counter.increase() + if not cls.is_stare_mode() and not cls.cyc_counter_stare.is_complete(): + cls.cyc_counter_stare.increase() - #Sets the state to stare if count completes - elif not cls.is_stare_mode() and cls.stare_counter.complete() and not cls.bypass_stare: + #Sets the state to stare if count is_completes + elif not cls.is_stare_mode() and cls.cyc_counter_stare.is_complete() and not cls.bypass_stare: cls.set_state("STARE") - print(f"State is {cls.state.name}") + + elif cls.cyc_counter_stare.active(): + cls.cyc_counter_stare.decrease() + + if not cls.cyc_counter_stare.active(): + if cls.bypass_stare: + cls.bypass_stare = False + if not cls.is_tracking_mode(): + cls.set_state("TRACKING") - elif cls.is_stare_mode(): - if cls.stare_counter.active(): - cls.stare_counter.decrease() - if not cls.stare_counter.active(): - if cls.bypass_stare: - cls.bypass_stare = False - print(f"bypass_stare disabled by check_for_stare") - cls.set_state("TRACKING") - print(f"State set to {cls.state.name}") - - elif not cls.stare_counter.active(): - cls.stare_counter.decrease() - @classmethod def check_for_inversion(cls): - if cls.dom_is_inward() and cls.rec_meets_thresh() and cls.is_stare_mode(): + if cls.dom_is_inward() and cls.rec_meets_thresh() and (cls.is_stare_mode() or cls.bypass_stare): #Updates the counter for activation - if not cls.is_inverted_mode() and not cls.inv_counter.complete(): - cls.inv_counter.increase() - #print(f"Inversion activation counter is increasing: {cls.inv_counter.get_count()}") + if not cls.is_inverted_mode() and not cls.cyc_counter_inv.is_complete(): + cls.cyc_counter_inv.increase() + #print(f"Inversion activation counter is increasing: {cls.cyc_counter_inv.get_count()}") - #Sets the state to inverted if enough cycles have completed - elif not cls.is_inverted_mode() and cls.inv_counter.complete(): + #Sets the state to inverted if enough cycles have is_completed + elif not cls.is_inverted_mode() and cls.cyc_counter_inv.is_complete(): cls.set_state("INVERTED") - print(f"State set to {cls.state.name}") cls.bypass_stare = True #Begins the counter for deactivation if conditions are not met - elif cls.is_inverted_mode(): - if cls.inv_counter.active(): - cls.inv_counter.decrease() - if not cls.inv_counter.active(): - if cls.bypass_stare: - cls.bypass_stare = False - print(f"bypass_stare disabled by check_for_inv") - - #Decreases the counter if conditions are not met, and inversion isn't active. - elif cls.inv_counter.active(): - cls.inv_counter.decrease() + elif cls.cyc_counter_inv.active(): + cls.cyc_counter_inv.decrease() + + if not cls.cyc_counter_inv.active(): + if cls.bypass_stare: + cls.bypass_stare = False + cls.cyc_counter_stare.force_complete() + cls.check_for_stare() @classmethod def update_new_position(cls,eye_id,out_x,out_y): @@ -206,6 +214,7 @@ class SmartInversion: @classmethod def set_state(cls,new_state: str): cls.state = cls.States[new_state] + print(f"State set to {cls.state.name}") @classmethod def is_dominant_eye(cls,eye_id): return eye_id == cls.dom_eye diff --git a/conftest.py b/conftest.py index dc8f541..e5784df 100644 --- a/conftest.py +++ b/conftest.py @@ -75,13 +75,15 @@ def eyetrack_settings_config(): gui_vrc_native=False, gui_pupil_dilation=True, - #Smart Inversion Tracking - gui_smartinversion_enabled=False, - gui_smartinversion_select_right=True, - gui_smartinversion_frame_count=10, - gui_smartinversion_smoothing_rate=0.025, - gui_smartinversion_minthresh=0.3, - gui_smartinversion_rotation_clamp=1.0, + #MirrorTrack + gui_mirrortrack_enabled=False, + gui_mirrortrack_select_right=True, + gui_mirrortrack_cycle_count_inv=20, + gui_mirrortrack_cycle_count_stare=20, + #gui_mirrortrack_smoothing_rate=0.025, + gui_mirrortrack_minthresh=0.3, + gui_mirrortrack_rotation_clamp=0.3, + gui_mirrortrack_enable_inv=False ) From 79403f365a48e5801709092f3c675bc0b99388d1 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 15 Feb 2025 16:41:23 +1300 Subject: [PATCH 21/27] Start of inversion smoothing implementation + clamp outwards movement during inversion --- EyeTrackApp/osc_calibrate_filter.py | 4 ++-- EyeTrackApp/utils/mirrortrack.py | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/EyeTrackApp/osc_calibrate_filter.py b/EyeTrackApp/osc_calibrate_filter.py index 4a85227..34af530 100644 --- a/EyeTrackApp/osc_calibrate_filter.py +++ b/EyeTrackApp/osc_calibrate_filter.py @@ -29,7 +29,7 @@ import time from enum import IntEnum from utils.misc_utils import PlaySound, SND_FILENAME, SND_ASYNC, resource_path from utils.eye_falloff import velocity_falloff -from utils.mirrortrack import mirrortrack +from utils.mirrortrack import MirrorTrack import socket import struct import threading @@ -338,7 +338,7 @@ class cal: var.past_y = out_y_mult if(self.settings.gui_mirrortrack_enabled): - out_x, out_y = mirrortrack.process(self.eye_id, out_x, out_y) + out_x, out_y = MirrorTrack.process(self.eye_id, out_x, out_y) else: out_x, out_y = velocity_falloff(self, var, out_x, out_y) diff --git a/EyeTrackApp/utils/mirrortrack.py b/EyeTrackApp/utils/mirrortrack.py index 632edc1..e0af779 100644 --- a/EyeTrackApp/utils/mirrortrack.py +++ b/EyeTrackApp/utils/mirrortrack.py @@ -1,10 +1,11 @@ from eye import EyeId from enum import Enum from config import EyeTrackConfig +from utils.misc_utils import clamp from utils.CycleCounter import * import threading -class mirrortrack: +class MirrorTrack: #Defines our possible tracking states class States(Enum): @@ -58,6 +59,8 @@ class mirrortrack: cls.cyc_counter_inv = CycleCounter(cls.cyc_counts_inv) cls.cyc_counter_stare = CycleCounter(cls.cyc_counts_stare) + cls.smoothing_rate = config.settings.gui_mirrortrack_ + cls.thread_lock = threading.Lock() #Receives changes in configuration and applies them accordingly @@ -195,9 +198,10 @@ class mirrortrack: if cls.is_inverted_mode(): if cls.is_processing_right_eye(eye_id): - out_x = max(out_x,-cls.inv_clamp) + out_x = clamp(out_x,-cls.inv_clamp,0) else: - out_x = min(out_x,cls.inv_clamp) + out_x = clamp(out_x,0,cls.inv_clamp) + return out_x, out_y @@ -237,12 +241,11 @@ class mirrortrack: def x_diff(cls): return abs(cls.rx_left_x - cls.rx_right_x) - """class Smoothing: - def __init__(self, smoothing_rate=0.05): - self.smoothing_rate = smoothing_rate - self.start_x = 0.0 - self.target_x = 0.0 - self.out - - def process_smoothing(self,out_x,out_y): - out_x = (self.start_x += (self.start_x - self.target_x) * smoothing_rate)""" \ No newline at end of file + @classmethod + def process_smoothing(cls,eye_id,out_x): + if cls.is_processing_right_eye(eye_id): + cls.tx_right_x += (out_x - cls.tx_right_x) * cls.smoothing_rate + out_x = cls.tx_right_x + else: + cls.tx_left_x += (out_x - cls.tx_left_x) * cls.smoothing_rate + out_x = cls.tx_left_x \ No newline at end of file From 28b6c58a4923077aad94f4b03c6e11963e2dd972 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sat, 15 Feb 2025 23:25:12 +1300 Subject: [PATCH 22/27] Fixed smoothing, fixed config_update issues, hid smoothing settings --- EyeTrackApp/config.py | 7 +- EyeTrackApp/eyetrackapp.py | 6 +- .../modules/MirrorTrackSettingsModule.py | 49 ++++--- EyeTrackApp/utils/mirrortrack.py | 122 +++++++++++------- conftest.py | 7 +- 5 files changed, 116 insertions(+), 75 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 9519c8f..e70751d 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -221,11 +221,12 @@ class EyeTrackSettingsConfig(BaseModel): gui_mirrortrack_enabled: bool = False gui_mirrortrack_select_right: bool = True gui_mirrortrack_cycle_count_inv: int = 20 - gui_mirrortrack_cycle_count_stare: int = 20 - #gui_mirrortrack_smoothing_rate: float = 0.025 + gui_mirrortrack_cycle_count_stare: int = 10 + gui_mirrortrack_smooth_rate: float = 0.2 gui_mirrortrack_minthresh: float = 0.3 gui_mirrortrack_rotation_clamp: float = 0.5 - gui_mirrortrack_enable_inv: bool = True + gui_mirrortrack_enable_inv: bool = True + gui_mirrortrack_enable_smooth: bool = True class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/eyetrackapp.py b/EyeTrackApp/eyetrackapp.py index 407b039..51ccc27 100644 --- a/EyeTrackApp/eyetrackapp.py +++ b/EyeTrackApp/eyetrackapp.py @@ -38,7 +38,7 @@ from settings.algo_settings_widget import AlgoSettingsWidget from osc.osc import OSCManager from osc.OSCMessage import OSCMessage from utils.misc_utils import is_nt, resource_path -from utils.mirrortrack import mirrortrack +from utils.mirrortrack import MirrorTrack import cv2 import numpy as np import uuid @@ -284,7 +284,7 @@ def main(): config.register_listener_callback(osc_manager.update) config.register_listener_callback(eyes[0].on_config_update) config.register_listener_callback(eyes[1].on_config_update) - config.register_listener_callback(mirrortrack.config_update) + config.register_listener_callback(MirrorTrack.config_update) osc_manager.register_listeners( config.settings.gui_osc_recenter_address, @@ -302,7 +302,7 @@ def main(): ) osc_manager.start() - mirrortrack.init_config(config) + MirrorTrack.init_config(config) while True: tint = 33 diff --git a/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py index 63f31b5..f70c092 100644 --- a/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py +++ b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py @@ -10,26 +10,28 @@ from settings.modules.CommonFieldValidators import try_convert_to_int class MirrorTrackValidationModule(BaseValidationModel): gui_mirrortrack_enabled: bool + gui_mirrortrack_enable_inv: bool + #gui_mirrortrack_enable_smooth: bool gui_mirrortrack_select_right: bool gui_mirrortrack_cycle_count_inv: Annotated[int, AfterValidator(try_convert_to_int)] gui_mirrortrack_cycle_count_stare: Annotated[int, AfterValidator(try_convert_to_int)] gui_mirrortrack_minthresh: Annotated[float, AfterValidator(try_convert_to_float)] gui_mirrortrack_rotation_clamp: Annotated[float, AfterValidator(try_convert_to_float)] - gui_mirrortrack_enable_inv: bool - #gui_mirrortrack_smoothing_rate: Annotated[float, AfterValidator(try_convert_to_float)] + gui_mirrortrack_smooth_rate: Annotated[float, AfterValidator(try_convert_to_float)] class MirrorTrackSettingsModule(BaseSettingsModule): def __init__(self, config, widget_id, **kwargs): super().__init__(config=config, widget_id=widget_id, **kwargs) self.validation_model = MirrorTrackValidationModule self.gui_mirrortrack_enabled = f"-gui_mirrortrack_enabled{widget_id}-" + self.gui_mirrortrack_enable_inv =f"-gui_mirrortrack_enable_inv{widget_id}-" + #self.gui_mirrortrack_enable_smooth =f"-gui_mirrortrack_enable_smooth{widget_id}-" self.gui_mirrortrack_select_right = f"-gui_mirrortrack_select_right{widget_id}-" self.gui_mirrortrack_cycle_count_inv =f"-gui_mirrortrack_cycle_count_inv{widget_id}-" self.gui_mirrortrack_cycle_count_stare =f"-gui_mirrortrack_cycle_count_stare{widget_id}-" self.gui_mirrortrack_minthresh =f"-gui_mirrortrack_minthresh{widget_id}-" self.gui_mirrortrack_rotation_clamp =f"-gui_mirrortrack_rotation_clamp{widget_id}-" - self.gui_mirrortrack_enable_inv =f"-gui_mirrortrack_enable_inv{widget_id}-" - #self.gui_mirrortrack_smoothing_rate =f"-gui_mirrortrack_smoothing_rate{widget_id}-" + self.gui_mirrortrack_smooth_rate =f"-gui_mirrortrack_smooth_rate{widget_id}-" def get_layout(self): return [ @@ -38,7 +40,7 @@ class MirrorTrackSettingsModule(BaseSettingsModule): ], [ sg.Checkbox( - "Enable:", + "Enable MirrorTrack", default=self.config.gui_mirrortrack_enabled, key=self.gui_mirrortrack_enabled, background_color="#424042", @@ -74,7 +76,7 @@ class MirrorTrackSettingsModule(BaseSettingsModule): ), ], [ - sg.Text("Transition Cycle Count (Cross Eye)", background_color=BACKGROUND_COLOR,tooltip= + sg.Text("Transition Cycle Count (Cross-Eye)", background_color=BACKGROUND_COLOR,tooltip= "How long it takes to detect you are cross-eyed, or no longer cross-eyed." "\n Higher number means longer duration before changing in or out of being cross-eyed state." ), @@ -95,28 +97,14 @@ class MirrorTrackSettingsModule(BaseSettingsModule): size=(0, 10), ), ], - #[ - # sg.Text("Smoothing Decay Rate", background_color=BACKGROUND_COLOR,tooltip= - # "How quickly eye smoothing decays when you enter or leave a cross-eyed state." - # "\nHigher number = shorter smoothing duration." - # ), - # sg.InputText( - # self.config.gui_mirrortrack_smoothing_rate, - # key=self.gui_mirrortrack_smoothing_rate, - # size=(0, 10), - # ), - #], - [ sg.Checkbox( - "Allow cross-eye:", + "Allow cross-eye", default=self.config.gui_mirrortrack_enable_inv, key=self.gui_mirrortrack_enable_inv, background_color="#424042", tooltip="Enables cross-eye functionality", ), - ], - [ sg.Text("Maximum allowed cross-eye", background_color=BACKGROUND_COLOR,tooltip= "Defines the maximum inwards rotation that is output when cross-eyed." "\n0 = will only look straight ahead \n0.5 = will go a little bit cross-eyed \n1 = maximum hurr durr " @@ -127,5 +115,24 @@ class MirrorTrackSettingsModule(BaseSettingsModule): size=(0, 10), ), ], + [ + #sg.Checkbox( + # "Allow cross-eye smoothing", + # default=self.config.gui_mirrortrack_enable_smooth, + # key=self.gui_mirrortrack_enable_smooth, + # background_color="#424042", + # tooltip="Enables smoothing when transitioning to cross-eye", + #), + sg.Text("Smoothing Rate", background_color=BACKGROUND_COLOR,tooltip= + "How quickly smoothing decays when you enter or leave a transition." + "\nHigher number = shorter smoothing duration, snappier transition." + "\nLower number = longer smoothing duration, smoother transition" + ), + sg.InputText( + self.config.gui_mirrortrack_smooth_rate, + key=self.gui_mirrortrack_smooth_rate, + size=(0, 10), + ), + ], ] \ No newline at end of file diff --git a/EyeTrackApp/utils/mirrortrack.py b/EyeTrackApp/utils/mirrortrack.py index e0af779..d8b6b14 100644 --- a/EyeTrackApp/utils/mirrortrack.py +++ b/EyeTrackApp/utils/mirrortrack.py @@ -25,7 +25,6 @@ class MirrorTrack: #Defines our processed positions tx_left_x = 0.0 - tx_left_y = 0.0 tx_right_x = 0.0 tx_right_y = 0.0 tx_dom_eye_x = 0.0 @@ -35,6 +34,7 @@ class MirrorTrack: inv_x_thresh = 0.3 is_r_dom = True bypass_stare = False + smoothing_trigger = False #Defines variables that require settings @classmethod @@ -49,7 +49,7 @@ class MirrorTrack: cls.is_r_dom = False #Inversion related - cls.inv_is_enabled = config.settings.gui_mirrortrack_enable_inv + cls.is_inv_enabled = config.settings.gui_mirrortrack_enable_inv cls.inv_x_thresh = config.settings.gui_mirrortrack_minthresh cls.inv_clamp = config.settings.gui_mirrortrack_rotation_clamp @@ -59,7 +59,8 @@ class MirrorTrack: cls.cyc_counter_inv = CycleCounter(cls.cyc_counts_inv) cls.cyc_counter_stare = CycleCounter(cls.cyc_counts_stare) - cls.smoothing_rate = config.settings.gui_mirrortrack_ + cls.is_smooth_enabled = config.settings.gui_mirrortrack_enable_smooth + cls.smoothing_rate = config.settings.gui_mirrortrack_smooth_rate cls.thread_lock = threading.Lock() @@ -78,13 +79,13 @@ class MirrorTrack: print(f"MirrorTrack transition threshold changed to {cls.inv_x_thresh}") if "gui_mirrortrack_cycle_count_inv" in data: - cls.cyc_counts_stare = data["gui_mirrortrack_cycle_count_inv"] - cls.cyc_counter_stare.update(cls.cyc_counts_stare) + cls.cyc_counts_inv = data["gui_mirrortrack_cycle_count_inv"] + cls.cyc_counter_inv.update(cls.cyc_counts_inv) print(f"MirrorTrack inversion transition condition required cycle count changed to {cls.cyc_counts_inv}") if "gui_mirrortrack_cycle_count_stare" in data: - cls.cyc_counts_inv = data["gui_mirrortrack_cycle_count_stare"] - cls.cyc_counter_stare.update(cls.cyc_counts_inv) + cls.cyc_counts_stare = data["gui_mirrortrack_cycle_count_stare"] + cls.cyc_counter_stare.update(cls.cyc_counts_stare) print(f"MirrorTrack stare transition condition required cycle count changed to {cls.cyc_counts_stare}") if "gui_mirrortrack_rotation_clamp" in data: @@ -92,8 +93,22 @@ class MirrorTrack: print(f"MirrorTrack maximum allowed cross-eye changed to {cls.inv_clamp}") if "gui_mirrortrack_enable_inv" in data: - cls.inv_is_enabled = data["gui_mirrortrack_enable_inv"] - print(f"MirrorTrack allow cross-eye is set to {cls.inv_is_enabled}") + cls.is_inv_enabled = data["gui_mirrortrack_enable_inv"] + + if not cls.is_inv_enabled and cls.is_inverted_mode(): + cls.set_state("STARE") + cls.bypass_stare = False + + print(f"MirrorTrack allow cross-eye is set to {cls.is_inv_enabled}") + + if "gui_mirrortrack_enable_smooth" in data: + cls.is_smooth_enabled = data["gui_mirrortrack_enable_smooth"] + print(f"MirrorTrack cross-eye smoothing is set to {cls.is_smooth_enabled}") + + if "gui_mirrortrack_smooth_rate" in data: + cls.smoothing_rate = data["gui_mirrortrack_smooth_rate"] + print(f"MirrorTrack cross-eye smoothing rate is set to {cls.smoothing_rate}") + #Main processing function @classmethod @@ -101,11 +116,12 @@ class MirrorTrack: with cls.thread_lock: cls.store_tracked_positions(eye_id,out_x,out_y) cls.check_for_stare() + cls.check_for_inversion() - if cls.inv_is_enabled: - cls.check_for_inversion() - out_x, out_y = cls.update_new_position(eye_id,out_x,out_y) + + out_x = cls.process_smoothing(eye_id,out_x) + cls.store_processed_positions(eye_id,out_x,out_y) return out_x, out_y @@ -141,9 +157,13 @@ class MirrorTrack: cls.tx_dom_eye_y = cls.tx_left_y @classmethod - def check_for_stare(cls): + def check_for_stare(cls,inv_call=False): if cls.dom_is_inward() and cls.rec_is_inward(): + #If inversion exits to stare, force complete the counter to force stare mode. + if inv_call: + cls.cyc_counter_stare.force_complete() + #Updates the counter for stare activation if not cls.is_stare_mode() and not cls.cyc_counter_stare.is_complete(): cls.cyc_counter_stare.increase() @@ -152,6 +172,10 @@ class MirrorTrack: elif not cls.is_stare_mode() and cls.cyc_counter_stare.is_complete() and not cls.bypass_stare: cls.set_state("STARE") + #If inversion exits and doesn't meet stare, reset stare counter to force tracking. + elif inv_call: + cls.cyc_counter_stare.reset() + elif cls.cyc_counter_stare.active(): cls.cyc_counter_stare.decrease() @@ -163,27 +187,32 @@ class MirrorTrack: @classmethod def check_for_inversion(cls): - if cls.dom_is_inward() and cls.rec_meets_thresh() and (cls.is_stare_mode() or cls.bypass_stare): + if cls.is_inv_enabled: + if cls.dom_is_inward() and cls.rec_meets_thresh() and (cls.is_stare_mode() or cls.bypass_stare): - #Updates the counter for activation - if not cls.is_inverted_mode() and not cls.cyc_counter_inv.is_complete(): - cls.cyc_counter_inv.increase() - #print(f"Inversion activation counter is increasing: {cls.cyc_counter_inv.get_count()}") - - #Sets the state to inverted if enough cycles have is_completed - elif not cls.is_inverted_mode() and cls.cyc_counter_inv.is_complete(): - cls.set_state("INVERTED") - cls.bypass_stare = True - - #Begins the counter for deactivation if conditions are not met - elif cls.cyc_counter_inv.active(): - cls.cyc_counter_inv.decrease() + #Updates the counter for activation + if not cls.is_inverted_mode() and not cls.cyc_counter_inv.is_complete(): + cls.cyc_counter_inv.increase() + #print(f"Inversion activation counter is increasing: {cls.cyc_counter_inv.get_count()}") + + #Sets the state to inverted if enough cycles have is_completed + elif not cls.is_inverted_mode() and cls.cyc_counter_inv.is_complete(): + cls.set_state("INVERTED") + cls.bypass_stare = True + cls.smoothing_trigger = True - if not cls.cyc_counter_inv.active(): - if cls.bypass_stare: - cls.bypass_stare = False - cls.cyc_counter_stare.force_complete() - cls.check_for_stare() + #Begins the counter for deactivation if conditions are not met + elif cls.cyc_counter_inv.active(): + cls.cyc_counter_inv.decrease() + + if not cls.cyc_counter_inv.active(): + if cls.bypass_stare: + cls.bypass_stare = False + if cls.is_inverted_mode(): + cls.smoothing_trigger = True + cls.check_for_stare(True) + else: + return @classmethod def update_new_position(cls,eye_id,out_x,out_y): @@ -202,9 +231,21 @@ class MirrorTrack: else: out_x = clamp(out_x,0,cls.inv_clamp) - return out_x, out_y - + + @classmethod + def process_smoothing(cls,eye_id,out_x): + if cls.is_smooth_enabled and cls.smoothing_trigger: + smoothing_out_x = cls.tx_right_x if cls.is_processing_right_eye(eye_id) else cls.tx_left_x + smoothing_out_x += (out_x - smoothing_out_x) * cls.smoothing_rate + + if abs(out_x - smoothing_out_x) < 0.1: + cls.smoothing_trigger = False + + return smoothing_out_x + + return out_x + #Helper Methods @classmethod def is_tracking_mode(cls): @@ -218,7 +259,7 @@ class MirrorTrack: @classmethod def set_state(cls,new_state: str): cls.state = cls.States[new_state] - print(f"State set to {cls.state.name}") + #print(f"State set to {cls.state.name}") @classmethod def is_dominant_eye(cls,eye_id): return eye_id == cls.dom_eye @@ -239,13 +280,4 @@ class MirrorTrack: return (cls.is_r_dom and cls.rx_left_x > cls.inv_x_thresh) or (not cls.is_r_dom and cls.rx_right_x < -cls.inv_x_thresh) @classmethod def x_diff(cls): - return abs(cls.rx_left_x - cls.rx_right_x) - - @classmethod - def process_smoothing(cls,eye_id,out_x): - if cls.is_processing_right_eye(eye_id): - cls.tx_right_x += (out_x - cls.tx_right_x) * cls.smoothing_rate - out_x = cls.tx_right_x - else: - cls.tx_left_x += (out_x - cls.tx_left_x) * cls.smoothing_rate - out_x = cls.tx_left_x \ No newline at end of file + return abs(cls.rx_left_x - cls.rx_right_x) \ No newline at end of file diff --git a/conftest.py b/conftest.py index e5784df..7601ce2 100644 --- a/conftest.py +++ b/conftest.py @@ -77,13 +77,14 @@ def eyetrack_settings_config(): #MirrorTrack gui_mirrortrack_enabled=False, + gui_mirrortrack_enable_inv=False + gui_mirrortrack_enable_smooth=True gui_mirrortrack_select_right=True, gui_mirrortrack_cycle_count_inv=20, gui_mirrortrack_cycle_count_stare=20, - #gui_mirrortrack_smoothing_rate=0.025, + gui_mirrortrack_smooth_rate=0.2, gui_mirrortrack_minthresh=0.3, - gui_mirrortrack_rotation_clamp=0.3, - gui_mirrortrack_enable_inv=False + gui_mirrortrack_rotation_clamp=0.5, ) From 55bcdec3cc22999cb630e48b8443026e49717366 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 16 Feb 2025 00:53:16 +1300 Subject: [PATCH 23/27] Make transition exit at 50% of count depletion speeds up exit states to look smoother --- EyeTrackApp/utils/CycleCounter.py | 5 ++++- EyeTrackApp/utils/mirrortrack.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/EyeTrackApp/utils/CycleCounter.py b/EyeTrackApp/utils/CycleCounter.py index 7fad7a5..b6d1411 100644 --- a/EyeTrackApp/utils/CycleCounter.py +++ b/EyeTrackApp/utils/CycleCounter.py @@ -33,4 +33,7 @@ class CycleCounter: self.max_count = max_count def force_complete(self): - self.count = self.max_count \ No newline at end of file + self.count = self.max_count + + def less_than_percentage(self,mult): + return self.count <= self.max_count * mult \ No newline at end of file diff --git a/EyeTrackApp/utils/mirrortrack.py b/EyeTrackApp/utils/mirrortrack.py index d8b6b14..f1b689b 100644 --- a/EyeTrackApp/utils/mirrortrack.py +++ b/EyeTrackApp/utils/mirrortrack.py @@ -179,7 +179,7 @@ class MirrorTrack: elif cls.cyc_counter_stare.active(): cls.cyc_counter_stare.decrease() - if not cls.cyc_counter_stare.active(): + if cls.cyc_counter_stare.less_than_percentage(0.5): if cls.bypass_stare: cls.bypass_stare = False if not cls.is_tracking_mode(): @@ -205,7 +205,7 @@ class MirrorTrack: elif cls.cyc_counter_inv.active(): cls.cyc_counter_inv.decrease() - if not cls.cyc_counter_inv.active(): + if cls.cyc_counter_inv.less_than_percentage(0.5): if cls.bypass_stare: cls.bypass_stare = False if cls.is_inverted_mode(): From 5fdd7421c5963943520115ed41584888b6e76ff2 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 16 Feb 2025 09:49:07 +1300 Subject: [PATCH 24/27] Hide debug prints, update default config values --- EyeTrackApp/config.py | 12 ++++++------ EyeTrackApp/utils/mirrortrack.py | 19 +++++++++++-------- conftest.py | 8 ++++---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index e70751d..4bdcb92 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -219,14 +219,14 @@ class EyeTrackSettingsConfig(BaseModel): #mirrortrackTracking gui_mirrortrack_enabled: bool = False - gui_mirrortrack_select_right: bool = True - gui_mirrortrack_cycle_count_inv: int = 20 - gui_mirrortrack_cycle_count_stare: int = 10 - gui_mirrortrack_smooth_rate: float = 0.2 - gui_mirrortrack_minthresh: float = 0.3 - gui_mirrortrack_rotation_clamp: float = 0.5 gui_mirrortrack_enable_inv: bool = True gui_mirrortrack_enable_smooth: bool = True + gui_mirrortrack_select_right: bool = True + gui_mirrortrack_cycle_count_inv: int = 15 + gui_mirrortrack_cycle_count_stare: int = 10 + gui_mirrortrack_smooth_rate: float = 0.2 + gui_mirrortrack_minthresh: float = 0.125 + gui_mirrortrack_rotation_clamp: float = 0.3 class EyeTrackConfig(BaseModel): version: int = 1 diff --git a/EyeTrackApp/utils/mirrortrack.py b/EyeTrackApp/utils/mirrortrack.py index f1b689b..0d8e253 100644 --- a/EyeTrackApp/utils/mirrortrack.py +++ b/EyeTrackApp/utils/mirrortrack.py @@ -72,25 +72,25 @@ class MirrorTrack: cls.dom_eye = EyeId.RIGHT if data["gui_mirrortrack_select_right"] else EyeId.LEFT cls.rec_eye = EyeId.LEFT if data["gui_mirrortrack_select_right"] else EyeId.RIGHT cls.is_r_dom = True if data["gui_mirrortrack_select_right"] else False - print(f"Dominant eye changed to {cls.dom_eye.name}") + #print(f"Dominant eye changed to {cls.dom_eye.name}") if "gui_mirrortrack_minthresh" in data: cls.inv_x_thresh = data["gui_mirrortrack_minthresh"] - print(f"MirrorTrack transition threshold changed to {cls.inv_x_thresh}") + #print(f"MirrorTrack transition threshold changed to {cls.inv_x_thresh}") if "gui_mirrortrack_cycle_count_inv" in data: cls.cyc_counts_inv = data["gui_mirrortrack_cycle_count_inv"] cls.cyc_counter_inv.update(cls.cyc_counts_inv) - print(f"MirrorTrack inversion transition condition required cycle count changed to {cls.cyc_counts_inv}") + #print(f"MirrorTrack inversion transition condition required cycle count changed to {cls.cyc_counts_inv}") if "gui_mirrortrack_cycle_count_stare" in data: cls.cyc_counts_stare = data["gui_mirrortrack_cycle_count_stare"] cls.cyc_counter_stare.update(cls.cyc_counts_stare) - print(f"MirrorTrack stare transition condition required cycle count changed to {cls.cyc_counts_stare}") + #print(f"MirrorTrack stare transition condition required cycle count changed to {cls.cyc_counts_stare}") if "gui_mirrortrack_rotation_clamp" in data: cls.inv_clamp = data["gui_mirrortrack_rotation_clamp"] - print(f"MirrorTrack maximum allowed cross-eye changed to {cls.inv_clamp}") + #print(f"MirrorTrack maximum allowed cross-eye changed to {cls.inv_clamp}") if "gui_mirrortrack_enable_inv" in data: cls.is_inv_enabled = data["gui_mirrortrack_enable_inv"] @@ -99,15 +99,15 @@ class MirrorTrack: cls.set_state("STARE") cls.bypass_stare = False - print(f"MirrorTrack allow cross-eye is set to {cls.is_inv_enabled}") + #print(f"MirrorTrack allow cross-eye is set to {cls.is_inv_enabled}") if "gui_mirrortrack_enable_smooth" in data: cls.is_smooth_enabled = data["gui_mirrortrack_enable_smooth"] - print(f"MirrorTrack cross-eye smoothing is set to {cls.is_smooth_enabled}") + #print(f"MirrorTrack cross-eye smoothing is set to {cls.is_smooth_enabled}") if "gui_mirrortrack_smooth_rate" in data: cls.smoothing_rate = data["gui_mirrortrack_smooth_rate"] - print(f"MirrorTrack cross-eye smoothing rate is set to {cls.smoothing_rate}") + #print(f"MirrorTrack cross-eye smoothing rate is set to {cls.smoothing_rate}") #Main processing function @@ -200,6 +200,7 @@ class MirrorTrack: cls.set_state("INVERTED") cls.bypass_stare = True cls.smoothing_trigger = True + #print(f"Smoothing trigger is {cls.smoothing_trigger}") #Begins the counter for deactivation if conditions are not met elif cls.cyc_counter_inv.active(): @@ -210,6 +211,7 @@ class MirrorTrack: cls.bypass_stare = False if cls.is_inverted_mode(): cls.smoothing_trigger = True + #print(f"Smoothing trigger is {cls.smoothing_trigger}") cls.check_for_stare(True) else: return @@ -241,6 +243,7 @@ class MirrorTrack: if abs(out_x - smoothing_out_x) < 0.1: cls.smoothing_trigger = False + #print(f"Smoothing trigger is {cls.smoothing_trigger}") return smoothing_out_x diff --git a/conftest.py b/conftest.py index 7601ce2..e73fd00 100644 --- a/conftest.py +++ b/conftest.py @@ -80,11 +80,11 @@ def eyetrack_settings_config(): gui_mirrortrack_enable_inv=False gui_mirrortrack_enable_smooth=True gui_mirrortrack_select_right=True, - gui_mirrortrack_cycle_count_inv=20, - gui_mirrortrack_cycle_count_stare=20, + gui_mirrortrack_cycle_count_inv=15, + gui_mirrortrack_cycle_count_stare=10, gui_mirrortrack_smooth_rate=0.2, - gui_mirrortrack_minthresh=0.3, - gui_mirrortrack_rotation_clamp=0.5, + gui_mirrortrack_minthresh=0.125, + gui_mirrortrack_rotation_clamp=0.3, ) From 92a4247d8f21c289a9ec9f2e3925b21cc188a36e Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 16 Feb 2025 09:53:41 +1300 Subject: [PATCH 25/27] Changed default eye to left, and ensured radio button is always one or the other --- EyeTrackApp/config.py | 2 +- EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py | 1 + conftest.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 4bdcb92..90a991f 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -221,7 +221,7 @@ class EyeTrackSettingsConfig(BaseModel): gui_mirrortrack_enabled: bool = False gui_mirrortrack_enable_inv: bool = True gui_mirrortrack_enable_smooth: bool = True - gui_mirrortrack_select_right: bool = True + gui_mirrortrack_select_right: bool = False gui_mirrortrack_cycle_count_inv: int = 15 gui_mirrortrack_cycle_count_stare: int = 10 gui_mirrortrack_smooth_rate: float = 0.2 diff --git a/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py index f70c092..676e097 100644 --- a/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py +++ b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py @@ -51,6 +51,7 @@ class MirrorTrackSettingsModule(BaseSettingsModule): sg.Radio( "Use Left Eye", "mirrortrack_selectedeye", + default = not self.config.gui_mirrortrack_select_right, background_color="#424042", tooltip="Uses the left eye as the tracked eye.", ), diff --git a/conftest.py b/conftest.py index e73fd00..d60222c 100644 --- a/conftest.py +++ b/conftest.py @@ -79,7 +79,7 @@ def eyetrack_settings_config(): gui_mirrortrack_enabled=False, gui_mirrortrack_enable_inv=False gui_mirrortrack_enable_smooth=True - gui_mirrortrack_select_right=True, + gui_mirrortrack_select_right=False, gui_mirrortrack_cycle_count_inv=15, gui_mirrortrack_cycle_count_stare=10, gui_mirrortrack_smooth_rate=0.2, From f6739c12ce306a3b6fbf365ac44b57ef3acac512 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 16 Feb 2025 09:58:00 +1300 Subject: [PATCH 26/27] Remove build compiler .bat from the fork --- .gitignore | 1 + EyeTrackApp/zBuild.bat | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 EyeTrackApp/zBuild.bat diff --git a/.gitignore b/.gitignore index c4fbf3d..55d10da 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ EyeTrackApp/IBO_RIGHT.png /eyetrack_settings.backup /eyetrack_settings.json zBuild.bat +EyeTrackApp/z_CompileBuild.bat diff --git a/EyeTrackApp/zBuild.bat b/EyeTrackApp/zBuild.bat deleted file mode 100644 index 9ddc2ac..0000000 --- a/EyeTrackApp/zBuild.bat +++ /dev/null @@ -1,2 +0,0 @@ -poetry run pyinstaller eyetrackapp.spec -cmd /k \ No newline at end of file From dbcfe8faaa94fc99a17954582802126a764388c5 Mon Sep 17 00:00:00 2001 From: Blabzillaweasel Date: Sun, 16 Feb 2025 13:13:32 +1300 Subject: [PATCH 27/27] Improved wording and layout of settings, and made inversion conditions a bit more robust --- .../modules/MirrorTrackSettingsModule.py | 59 ++++++++++--------- EyeTrackApp/utils/mirrortrack.py | 2 +- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py index 676e097..838dc1b 100644 --- a/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py +++ b/EyeTrackApp/settings/modules/MirrorTrackSettingsModule.py @@ -66,31 +66,11 @@ class MirrorTrackSettingsModule(BaseSettingsModule): ) ], [ - sg.Text("Inwards Look Threshold", background_color=BACKGROUND_COLOR,tooltip= - "Sets the minimum distance of looking in that's required before state can chaned to cross-eyed." - "\n Lower value will make cross-eye detection more sensitive." - ), - sg.InputText( - self.config.gui_mirrortrack_minthresh, - key=self.gui_mirrortrack_minthresh, - size=(0, 10), - ), - ], - [ - sg.Text("Transition Cycle Count (Cross-Eye)", background_color=BACKGROUND_COLOR,tooltip= - "How long it takes to detect you are cross-eyed, or no longer cross-eyed." - "\n Higher number means longer duration before changing in or out of being cross-eyed state." - ), - sg.InputText( - self.config.gui_mirrortrack_cycle_count_inv, - key=self.gui_mirrortrack_cycle_count_inv, - size=(0, 10), - ), - ], - [ - sg.Text("Transition Cycle Count (Stare Forward)", background_color=BACKGROUND_COLOR,tooltip= + sg.Text("Stare Ahead Detection Duration", background_color=BACKGROUND_COLOR,tooltip= "How long it takes to detect you are staring ahead, or no longer staring ahead." "\n Higher number means longer duration before changing in or out of being in stare ahead state." + "\n Exit conditions are half the duration of entry conditions." + ), sg.InputText( self.config.gui_mirrortrack_cycle_count_stare, @@ -100,13 +80,38 @@ class MirrorTrackSettingsModule(BaseSettingsModule): ], [ sg.Checkbox( - "Allow cross-eye", + "Enable Cross-Eye Detection", default=self.config.gui_mirrortrack_enable_inv, key=self.gui_mirrortrack_enable_inv, background_color="#424042", tooltip="Enables cross-eye functionality", ), - sg.Text("Maximum allowed cross-eye", background_color=BACKGROUND_COLOR,tooltip= + ], + [ + sg.Text("Detection Threshold", background_color=BACKGROUND_COLOR,tooltip= + "Sets the minimum distance of looking in that's required before state will changed to cross-eyed." + "\n Lower value will make cross-eye detection more sensitive." + ), + sg.InputText( + self.config.gui_mirrortrack_minthresh, + key=self.gui_mirrortrack_minthresh, + size=(0, 10), + ), + ], + [ + sg.Text("Detection Duration", background_color=BACKGROUND_COLOR,tooltip= + "How long it takes to detect you are cross-eyed, or no longer cross-eyed." + "\n Higher number means longer duration before changing in or out of being cross-eyed state." + "\n Exit conditions are half the duration of entry conditions." + ), + sg.InputText( + self.config.gui_mirrortrack_cycle_count_inv, + key=self.gui_mirrortrack_cycle_count_inv, + size=(0, 10), + ), + ], + [ + sg.Text("Rotation Limit", background_color=BACKGROUND_COLOR,tooltip= "Defines the maximum inwards rotation that is output when cross-eyed." "\n0 = will only look straight ahead \n0.5 = will go a little bit cross-eyed \n1 = maximum hurr durr " ), @@ -124,8 +129,8 @@ class MirrorTrackSettingsModule(BaseSettingsModule): # background_color="#424042", # tooltip="Enables smoothing when transitioning to cross-eye", #), - sg.Text("Smoothing Rate", background_color=BACKGROUND_COLOR,tooltip= - "How quickly smoothing decays when you enter or leave a transition." + sg.Text("Transition Smoothing Rate", background_color=BACKGROUND_COLOR,tooltip= + "How quickly smoothing decays when you enter or leave the cross-eyed state." "\nHigher number = shorter smoothing duration, snappier transition." "\nLower number = longer smoothing duration, smoother transition" ), diff --git a/EyeTrackApp/utils/mirrortrack.py b/EyeTrackApp/utils/mirrortrack.py index 0d8e253..13d3264 100644 --- a/EyeTrackApp/utils/mirrortrack.py +++ b/EyeTrackApp/utils/mirrortrack.py @@ -188,7 +188,7 @@ class MirrorTrack: @classmethod def check_for_inversion(cls): if cls.is_inv_enabled: - if cls.dom_is_inward() and cls.rec_meets_thresh() and (cls.is_stare_mode() or cls.bypass_stare): + if cls.dom_meets_thresh() and cls.rec_meets_thresh() and (cls.is_stare_mode() or cls.bypass_stare): #Updates the counter for activation if not cls.is_inverted_mode() and not cls.cyc_counter_inv.is_complete():