From 172b4df68a2981bee756c6a20cc79e0f72a3e44c Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 10 May 2023 10:13:48 +0200 Subject: Improving visual scan step creation. Adding VisualScanStepError. --- src/argaze/GazeFeatures.py | 93 +++++++++++++++++++++++++++++++++++----------- 1 file 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') -- cgit v1.1