From 47836903ccd6dbb91baf340777f1ddf9d73e8cb6 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Sat, 10 Dec 2022 23:37:44 +0100 Subject: Refactoring movement identification. --- .../DispersionBasedGazeMovementIdentifier.py | 207 ++++++++++++--------- 1 file changed, 114 insertions(+), 93 deletions(-) diff --git a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py b/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py index 62e8723..9373c4f 100644 --- a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py +++ b/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py @@ -105,95 +105,91 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # While there are 2 gaze positions at least while len(self.__ts_gaze_positions) >= 2: - # Copy remaining gaze positions - remaining_gaze_positions = self.__ts_gaze_positions.copy() + # Remove all unvalid gaze positions until to find a valid one + ts_current, gaze_position_current = self.__ts_gaze_positions.pop_first() - # Select gaze position until a duration threshold - ts_start, gaze_position_start = remaining_gaze_positions.pop_first() - - # Ignore and remove non valid start positions - while not gaze_position_start.valid: - - ts_start, gaze_position_start = remaining_gaze_positions.pop_first() - self.__ts_gaze_positions.pop_first() + while not gaze_position_current.valid and len(self.__ts_gaze_positions) > 0: + ts_current, gaze_position_current = self.__ts_gaze_positions.pop_first() + # Select current and next valid gaze positions until a duration threshold valid_gaze_positions = GazeFeatures.TimeStampedGazePositions() - valid_gaze_positions[ts_start] = gaze_position_start + valid_gaze_positions[ts_current] = gaze_position_current + # Store unvalid gaze positions to count them unvalid_gaze_positions = GazeFeatures.TimeStampedGazePositions() - # Select next position - ts_next, gaze_position_next = remaining_gaze_positions.first + for ts_next, gaze_position_next in self.__ts_gaze_positions.items(): - while (ts_next - ts_start) < self.duration_threshold: + if (ts_next - ts_current) < self.duration_threshold: - # Store valid position - if gaze_position_next.valid: + # Store valid position + if gaze_position_next.valid: - ts, valid_gaze_position = remaining_gaze_positions.pop_first() - valid_gaze_positions[ts] = valid_gaze_position + valid_gaze_positions[ts_next] = gaze_position_next - # Store non valid position - else: + # Store non valid position + else: - ts, unvalid_gaze_position = remaining_gaze_positions.pop_first() - unvalid_gaze_positions[ts] = unvalid_gaze_position + unvalid_gaze_positions[ts_next] = gaze_position_next - try: - # Read next position - ts_next, gaze_position_next = remaining_gaze_positions.first + else: - except: break - # Consider the last valid gaze positions as a new fixation - new_fixation = Fixation(valid_gaze_positions) + # If there is at least 2 valid gaze positions selected + if len(valid_gaze_positions) >= 2: - # Dispersion is small : extending fixation - if new_fixation.dispersion <= self.dispersion_threshold: + # Consider selected valid gaze positions as part of a maybe new fixation + new_fixation = Fixation(valid_gaze_positions) - # Remove valid gaze positions - for ts, gp in valid_gaze_positions.items(): - self.__ts_gaze_positions.pop(ts) + # Dispersion small enough: it is a fixation ! Try to extend it + if new_fixation.dispersion <= self.dispersion_threshold: - # Remove unvalid gaze positions - for ts, gp in unvalid_gaze_positions.items(): - self.__ts_gaze_positions.pop(ts) + # Remove valid and unvalid gaze positions as there as now stored in new fixation + # -1 as current gaze position have already been poped + for _ in range(len(valid_gaze_positions) + len(unvalid_gaze_positions) - 1): + self.__ts_gaze_positions.pop_first() - # Copy new fixation positions before to try to extend them - extended_gaze_positions = new_fixation.positions.copy() + # Copy new fixation positions before to try to extend them + extended_gaze_positions = new_fixation.positions.copy() - # Are next gaze positions not too dispersed ? - while len(remaining_gaze_positions) > 0: + # Are next gaze positions not too dispersed ? + while len(self.__ts_gaze_positions) > 0: - # Select next gaze position - ts_next, gaze_position_next = remaining_gaze_positions.first + # Select next gaze position + ts_next, gaze_position_next = self.__ts_gaze_positions.first - # Ignore and remove non valid next positions - if not gaze_position_next.valid: + # Consider only valid next position + if gaze_position_next.valid: - ts_next, gaze_position_next = remaining_gaze_positions.pop_first() - self.__ts_gaze_positions.pop_first() - continue + extended_gaze_positions[ts_next] = gaze_position_next - extended_gaze_positions[ts_next] = gaze_position_next + # How much extended fixation is dispersed ? + extended_fixation = Fixation(extended_gaze_positions) - # how much gaze is dispersed ? - extended_fixation = Fixation(extended_gaze_positions) + # Dispersion is still small enough : continue + if extended_fixation.dispersion < self.dispersion_threshold: + + # Remove selected gaze position + self.__ts_gaze_positions.pop_first() + + continue + + # Dispersion is too wide : break + else: - # dispersion becomes too wide : ignore extended fixation - if extended_fixation.dispersion > self.dispersion_threshold: - break + # Remove last extended gaze position + extended_gaze_positions.pop_last() - # update new fixation - new_fixation = Fixation(extended_gaze_positions.copy()) + break - # remove selected gaze position - remaining_gaze_positions.pop_first() - self.__ts_gaze_positions.pop_first() + # Remove non valid positions and continue + else: + + self.__ts_gaze_positions.pop_first() - # Is the new fixation have a duration ? - if new_fixation.duration > 0: + # Update new fixation + new_fixation = Fixation(extended_gaze_positions) # Does a former fixation have been identified ? if self.__last_fixation != None: @@ -207,48 +203,69 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Else output last fixation and consider moving gaze positions buffer else: - yield self.__last_fixation + # Saccade happens between last and new fixations + last_ts, _ = self.__last_fixation.positions.last + new_ts, _ = new_fixation.positions.first - # Is there a new movement ? - if len(moving_gaze_positions) > 0: + # Saccade shouldn't be longer than fixation + # TODO : add a saccade duration threshold attribute ? + if new_ts - last_ts <= self.duration_threshold: - # Compare first and last gaze position of the moving positions buffer to last and new fixation - start_position_ts, start_position = moving_gaze_positions.first - end_position_ts, end_position = moving_gaze_positions.last + # Edit saccade gaze positions + saccade_gaze_positions = GazeFeatures.TimeStampedGazePositions() + + # Edit first saccade gaze position + last_ts, last_position = self.__last_fixation.positions.pop_last() + saccade_gaze_positions[last_ts] = last_position + + # Is there unmatched gaze positions that belong to the saccade? + if len(moving_gaze_positions) > 0: + + start_position_ts, start_position = moving_gaze_positions.first + end_position_ts, end_position = moving_gaze_positions.last - last_ts, last_position = self.__last_fixation.positions.last - new_ts, new_position = new_fixation.positions.first + if start_position_ts > last_ts and end_position_ts < new_ts: - # Saccade conditions: - # - should be between two fixations - # - shouldn't be longer than fixation - # TODO : add a saccade duration threshold attribute ? - if start_position_ts > last_ts and end_position_ts < new_ts and last_ts - new_ts <= self.duration_threshold: + # Append unmatched gaze positions to saccade + saccade_gaze_positions.append(moving_gaze_positions) - saccade_gaze_positions = GazeFeatures.TimeStampedGazePositions() - saccade_gaze_positions[last_ts] = last_position - saccade_gaze_positions.append(moving_gaze_positions) - saccade_gaze_positions[new_ts] = new_position + # Edit last saccade gaze psoition + new_ts, new_position = new_fixation.positions.pop_first() + saccade_gaze_positions[new_ts] = new_position + + # Output last fixation + yield self.__last_fixation - yield Saccade(saccade_gaze_positions) + # Output saccade + yield Saccade(saccade_gaze_positions) - # Otherwise, this movement is unknown - else: + # Too much time between fixations: no saccade + # But if there are unmatched gaze positions, this movement is unknown + elif len(moving_gaze_positions) > 0: - # Does this unknown movement an unmatched fixation ? - unmatched_fixation = Fixation(moving_gaze_positions) + # Is this unknown movement happened between the last and the new fixation ? + start_position_ts, start_position = moving_gaze_positions.first + end_position_ts, end_position = moving_gaze_positions.last + + if start_position_ts > last_ts and end_position_ts < new_ts: - if unmatched_fixation.dispersion < self.dispersion_threshold: + # Output last fixation + yield self.__last_fixation - yield unmatched_fixation + # Output unknown movement + yield UnknownGazeMovement(moving_gaze_positions) - else: + # The unknown movement happened before last fixation + else: - yield UnknownGazeMovement(moving_gaze_positions) + # QUESTION: What to do in this case? + # GazeMovementIdentifier have to output movements according their time apparition + pass - # Forget former moving gaze positions - moving_gaze_positions = GazeFeatures.TimeStampedGazePositions() + # Forget former moving gaze positions + moving_gaze_positions = GazeFeatures.TimeStampedGazePositions() + # New fixation becomes the last fixation to allow further merging self.__last_fixation = new_fixation else: @@ -259,13 +276,17 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Forget former moving gaze positions moving_gaze_positions = GazeFeatures.TimeStampedGazePositions() - # Dispersion too wide : - # - This gaze position is part of a movement but not a fixation - # - Consider next gaze position + # Dispersion too wide: + # Current gaze position is not part of a fixation + else: + + moving_gaze_positions[ts_current] = gaze_position_current + + # Only one valid gaze position selected: + # Current gaze position is not part of a fixation else: - ts_moving, moving_gaze_position = self.__ts_gaze_positions.pop_first() - moving_gaze_positions[ts_moving] = moving_gaze_position + moving_gaze_positions[ts_current] = gaze_position_current # Output last fixation if self.__last_fixation != None: -- cgit v1.1