aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThéo de la Hogue2022-12-11 05:15:43 +0100
committerThéo de la Hogue2022-12-11 05:15:43 +0100
commit447397a3703dd03982c464bce7d3c9cb4a6ced6b (patch)
tree78f4c7b372fbe95e25859edae54c847dd8590a44 /src
parent63c302e81a9fb94b6953b1d71e4da80606dd8752 (diff)
downloadargaze-447397a3703dd03982c464bce7d3c9cb4a6ced6b.zip
argaze-447397a3703dd03982c464bce7d3c9cb4a6ced6b.tar.gz
argaze-447397a3703dd03982c464bce7d3c9cb4a6ced6b.tar.bz2
argaze-447397a3703dd03982c464bce7d3c9cb4a6ced6b.tar.xz
Major movement identification improvements.
Diffstat (limited to 'src')
-rw-r--r--src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py163
1 files 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