aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/argaze/GazeFeatures.py93
1 files changed, 72 insertions, 21 deletions
diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py
index 7afb7ba..7f292ee 100644
--- a/src/argaze/GazeFeatures.py
+++ b/src/argaze/GazeFeatures.py
@@ -289,11 +289,18 @@ class GazeMovementIdentifier():
VisualScanStepType = TypeVar('VisualScanStep', bound="VisualScanStep")
# Type definition for type annotation convenience
+class VisualScanStepError(Exception):
+ """Exception raised at VisualScanStepError creation if a visual scan step doesn't start by a fixation or doesn't end by a saccade."""
+
+ def __init__(self, message):
+
+ super().__init__(message)
+
@dataclass(frozen=True)
class VisualScanStep():
"""Define a visual scan step as a set of successive gaze movements onto a same AOI.
- .. warning::
- Last movement have to be a saccade that comes out AOI."""
+ .. warning::
+ Visual scan step have to start by a fixation and then end by a saccade."""
movements: TimeStampedGazeMovements
"""All movements over an AOI and the last saccade that comes out."""
@@ -301,18 +308,50 @@ class VisualScanStep():
aoi: str = field(default='')
"""AOI name."""
+ #identifier: int = field(default=None)
+ """AOI identifier."""
+
def __post_init__(self):
+ # First movement have to be a fixation
+ if type(self.first_fixation).__bases__[0] != Fixation:
+
+ raise VisualScanStepError('First step movement is not a fixation')
+
# Last movement have to be a saccade
- assert(type(self.transition).__bases__[0] == Saccade)
+ if type(self.last_saccade).__bases__[0] != Saccade:
+
+ raise VisualScanStepError('Last step movement is not a saccade')
@property
- def transition(self):
+ def first_fixation(self):
+ """First fixation on AOI."""
+
+ _, first_movement = self.movements.first
+ return first_movement
+
+ @property
+ def last_saccade(self):
"""Last saccade that comes out AOI."""
_, last_movement = self.movements.last
return last_movement
+ @property
+ def duration(self):
+ """Time spent on AOI."""
+
+ # Timestamp of first position of first fixation
+ first_ts, _ = self.movements.first.positions.first
+
+ # Timestamp of first position of last saccade
+ last_ts, _ = self.movements.last.positions.first
+
+ return last_ts - first_ts
+
+VisualScanType = TypeVar('VisualScanType', bound="VisualScanType")
+# Type definition for type annotation convenience
+
class VisualScan(list):
"""List visual scan steps over successive aoi."""
@@ -338,47 +377,59 @@ class VisualScan(list):
output += f'> {step.aoi} '
return output
-
+
def append_saccade(self, ts, saccade):
"""Append new saccade to visual scan."""
- self.__movements[ts] = saccade
+ # Ignore saccade if no fixation have been stored before
+ if len(self.__movements) > 0:
+
+ self.__movements[ts] = saccade
def append_fixation(self, ts, fixation, looked_aoi: str) -> bool:
- """Append new fixation to visual scan and return last new visual scan step if one have been created."""
+ """Append new fixation to visual scan and return last new visual scan step if one have been created.
+
+ .. warning::
+ It could raise VisualScanStepError"""
- # Is the fixation onto a new aoi?
+ # Is it fixation onto a new aoi?
if looked_aoi != self.__last_aoi and len(self.__movements) > 0:
- # Edit new step
- new_step = VisualScanStep(self.__movements, looked_aoi)
+ try:
- # Append new step
- super().append(new_step)
+ # Edit new step
+ new_step = VisualScanStep(self.__movements, looked_aoi)
- # Clear movements
- self.__movements = TimeStampedGazeMovements()
+ # Append new step
+ super().append(new_step)
- # Append new fixation
- self.__movements[ts] = fixation
+ # Return new step
+ return new_step
- # Remember new aoi
- self.__last_aoi = looked_aoi
+ finally:
- # Return new step
- return new_step
+ # Clear movements
+ self.__movements = TimeStampedGazeMovements()
+ # Append new fixation
+ self.__movements[ts] = fixation
+
+ # Remember new aoi
+ self.__last_aoi = looked_aoi
else:
# Append new fixation
self.__movements[ts] = fixation
+ # Remember aoi
+ self.__last_aoi = looked_aoi
+
return None
class VisualScanAnalyzer():
"""Abstract class to define what should provide a visual scan analyzer."""
- def analyze(self, visual_scan: list[VisualScanStepType]) -> Any:
+ def analyze(self, visual_scan: VisualScanType) -> Any:
"""Analyze visual scan."""
raise NotImplementedError('analyze() method not implemented')