From 447397a3703dd03982c464bce7d3c9cb4a6ced6b Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Sun, 11 Dec 2022 05:15:43 +0100 Subject: Major movement identification improvements. --- .../DispersionBasedGazeMovementIdentifier.py | 163 +++++++++++---------- 1 file changed, 85 insertions(+), 78 deletions(-) diff --git a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py b/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py index 9373c4f..e205189 100644 --- a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py +++ b/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py @@ -24,8 +24,12 @@ class Fixation(GazeFeatures.Fixation): super().__post_init__() - x_list = [gp[0] for (ts, gp) in list(self.positions.items())] - y_list = [gp[1] for (ts, gp) in list(self.positions.items())] + self.update() + + def update(self): + + x_list = [gp[0] for (_, gp) in list(self.positions.items())] + y_list = [gp[1] for (_, gp) in list(self.positions.items())] cx = numpy.mean(x_list) cy = numpy.mean(y_list) @@ -60,6 +64,14 @@ class Fixation(GazeFeatures.Fixation): return dist < (self.dispersion + fixation.dispersion) + def contains_point(self, gaze_position) -> bool: + """Is a point inside fixation?""" + + dist = (self.centroid[0] - gaze_position[0])**2 + (self.centroid[1] - gaze_position[1])**2 + dist = numpy.sqrt(dist) + + return dist < self.dispersion + def merge(self, fixation) -> float: """Merge another fixation into this fixation.""" @@ -100,7 +112,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): """GazeMovement identification generator.""" self.__last_fixation = None - moving_gaze_positions = GazeFeatures.TimeStampedGazePositions() + unmatched_gaze_positions = GazeFeatures.TimeStampedGazePositions() # While there are 2 gaze positions at least while len(self.__ts_gaze_positions) >= 2: @@ -152,12 +164,13 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Copy new fixation positions before to try to extend them extended_gaze_positions = new_fixation.positions.copy() + extended_fixation = new_fixation # Are next gaze positions not too dispersed ? while len(self.__ts_gaze_positions) > 0: - # Select next gaze position - ts_next, gaze_position_next = self.__ts_gaze_positions.first + # Select and remove next gaze position + ts_next, gaze_position_next = self.__ts_gaze_positions.pop_first() # Consider only valid next position if gaze_position_next.valid: @@ -167,128 +180,122 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # How much extended fixation 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: - - # Remove last extended gaze position - extended_gaze_positions.pop_last() + if extended_fixation.dispersion > self.dispersion_threshold: break - # Remove non valid positions and continue - else: - - self.__ts_gaze_positions.pop_first() + # NOTE : The last extended position is out of the fixation : this position will be popped later # Update new fixation - new_fixation = Fixation(extended_gaze_positions) + new_fixation = extended_fixation # Does a former fixation have been identified ? if self.__last_fixation != None: - # Merge new fixation if it overlaps last fixation - if self.__last_fixation.overlap(new_fixation): + # Inter fixations movement should: + # - starts at last position of last fixation (this position is out so it have to be popped) + # - stops at the first position inside new fixation + start_movement_ts, start_position = self.__last_fixation.positions.pop_last() + stop_movement_ts, stop_position = new_fixation.positions.pop_first() - self.__last_fixation.merge(new_fixation) - new_fixation = None + # Rare case : the last fixation position is the same than the first position of the new fixation + if start_movement_ts == stop_movement_ts: + start_movement_ts, start_position = self.__last_fixation.positions.pop_last() - # Else output last fixation and consider moving gaze positions buffer - else: + # Update last and new fixations + self.__last_fixation.update() + new_fixation.update() - # Saccade happens between last and new fixations - last_ts, _ = self.__last_fixation.positions.last - new_ts, _ = new_fixation.positions.first + # Edit inter movement gaze positions + movement_gaze_positions = GazeFeatures.TimeStampedGazePositions() - # Saccade shouldn't be longer than fixation - # TODO : add a saccade duration threshold attribute ? - if new_ts - last_ts <= self.duration_threshold: + # Edit first movement gaze position + movement_gaze_positions[start_movement_ts] = start_position - # Edit saccade gaze positions - saccade_gaze_positions = GazeFeatures.TimeStampedGazePositions() + # Is there unmatched gaze positions that belong to the movement? + if len(unmatched_gaze_positions) > 0: - # Edit first saccade gaze position - last_ts, last_position = self.__last_fixation.positions.pop_last() - saccade_gaze_positions[last_ts] = last_position + start_unmatched_ts, _ = unmatched_gaze_positions.first + end_unmatched_ts, _ = unmatched_gaze_positions.last - # Is there unmatched gaze positions that belong to the saccade? - if len(moving_gaze_positions) > 0: + # Does unmatched gaze positions happened between the last and the new fixation ? + if start_unmatched_ts > start_movement_ts and end_unmatched_ts < stop_movement_ts: - start_position_ts, start_position = moving_gaze_positions.first - end_position_ts, end_position = moving_gaze_positions.last + # Append unmatched gaze positions to saccade + movement_gaze_positions.append(unmatched_gaze_positions) - if start_position_ts > last_ts and end_position_ts < new_ts: + # Unmatched gaze positions happened before last fixation + else: - # Append unmatched gaze positions to saccade - saccade_gaze_positions.append(moving_gaze_positions) + # Ignore them: GazeMovementIdentifier have to output movements according their time apparition + pass - # Edit last saccade gaze psoition - new_ts, new_position = new_fixation.positions.pop_first() - saccade_gaze_positions[new_ts] = new_position + # Edit last movement gaze position + movement_gaze_positions[stop_movement_ts] = stop_position - # Output last fixation - yield self.__last_fixation - - # Output saccade - yield Saccade(saccade_gaze_positions) + # Short time between fixations : + # this movement is a saccade unless last and new fixations overlap + if stop_movement_ts - start_movement_ts <= self.duration_threshold: - # 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 new fixation overlap last fixation? + if self.__last_fixation.overlap(new_fixation): - # 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 + merged_positions = self.__last_fixation.positions + merged_positions.append(movement_gaze_positions) + merged_positions.append(new_fixation.positions) - if start_position_ts > last_ts and end_position_ts < new_ts: + self.__last_fixation = Fixation(merged_positions) - # Output last fixation - yield self.__last_fixation + # Forget new fixation + new_fixation = None - # Output unknown movement - yield UnknownGazeMovement(moving_gaze_positions) + else: - # The unknown movement happened before last fixation - else: + # Output last fixation + yield self.__last_fixation - # QUESTION: What to do in this case? - # GazeMovementIdentifier have to output movements according their time apparition - pass + # New fixation becomes the last fixation to allow further merging + self.__last_fixation = new_fixation - # Forget former moving gaze positions - moving_gaze_positions = GazeFeatures.TimeStampedGazePositions() + # Output saccade + yield Saccade(movement_gaze_positions) + + # Too much time between fixations: this movement is unknown + else: + + # Output last fixation + yield self.__last_fixation # New fixation becomes the last fixation to allow further merging self.__last_fixation = new_fixation + # Output unknown movement + yield UnknownGazeMovement(movement_gaze_positions) + + # In any case, forget former unmatched gaze positions + unmatched_gaze_positions = GazeFeatures.TimeStampedGazePositions() + + # First fixation is stored to allow further merging else: self.__last_fixation = new_fixation - yield self.__last_fixation - # Forget former moving gaze positions - moving_gaze_positions = GazeFeatures.TimeStampedGazePositions() + # Forget former unmatched gaze positions + unmatched_gaze_positions = GazeFeatures.TimeStampedGazePositions() # Dispersion too wide: # Current gaze position is not part of a fixation else: - moving_gaze_positions[ts_current] = gaze_position_current + unmatched_gaze_positions[ts_current] = gaze_position_current # Only one valid gaze position selected: # Current gaze position is not part of a fixation else: - moving_gaze_positions[ts_current] = gaze_position_current + unmatched_gaze_positions[ts_current] = gaze_position_current # Output last fixation if self.__last_fixation != None: - yield self.__last_fixation -- cgit v1.1