aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThéo de la Hogue2023-07-10 15:10:06 +0200
committerThéo de la Hogue2023-07-10 15:10:06 +0200
commiteccb31a63ce5196023810e31f6c70dcde6c151ec (patch)
tree69382f0cd437181a7208925df119e3b966581a86 /src
parent26f4046baa96ea50185d9d7905bd23ab5591b983 (diff)
downloadargaze-eccb31a63ce5196023810e31f6c70dcde6c151ec.zip
argaze-eccb31a63ce5196023810e31f6c70dcde6c151ec.tar.gz
argaze-eccb31a63ce5196023810e31f6c70dcde6c151ec.tar.bz2
argaze-eccb31a63ce5196023810e31f6c70dcde6c151ec.tar.xz
Improving and testing velocity threshold identification
Diffstat (limited to 'src')
-rw-r--r--src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py112
-rw-r--r--src/argaze/GazeAnalysis/VelocityThresholdIdentification.py47
2 files changed, 94 insertions, 65 deletions
diff --git a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py
index 71a8daf..239bf45 100644
--- a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py
+++ b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py
@@ -30,6 +30,10 @@ def build_gaze_fixation(size: int, start_position: tuple, deviation_max: float,
for i in range(0, size):
+ # Sleep a random time
+ sleep_time = random.random() * (max_time - min_time) + min_time
+ time.sleep(sleep_time)
+
# Check position validity
valid = True
if len(validity) > i:
@@ -39,8 +43,8 @@ def build_gaze_fixation(size: int, start_position: tuple, deviation_max: float,
if valid:
# Edit gaze position
- random_x = last_valid_position[0] + deviation_max * (random.random() - 0.5)
- random_y = last_valid_position[1] + deviation_max * (random.random() - 0.5)
+ random_x = last_valid_position[0] + deviation_max * (random.random() - 0.5) / math.sqrt(2)
+ random_y = last_valid_position[1] + deviation_max * (random.random() - 0.5) / math.sqrt(2)
gaze_position = GazeFeatures.GazePosition((random_x, random_y))
@@ -55,10 +59,6 @@ def build_gaze_fixation(size: int, start_position: tuple, deviation_max: float,
ts = time.time() - start_time + start_ts
ts_gaze_positions[ts] = gaze_position
- # Sleep a random time
- sleep_time = random.random() * (max_time - min_time) + min_time
- time.sleep(sleep_time)
-
return ts_gaze_positions
def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: float, max_time: float, start_ts: float = 0., validity: list = []):
@@ -72,6 +72,10 @@ def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: fl
for i in range(0, size):
+ # Sleep a random time
+ sleep_time = random.random() * (max_time - min_time) + min_time
+ time.sleep(sleep_time)
+
# Check position validity
valid = True
if len(validity) > i:
@@ -93,10 +97,6 @@ def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: fl
ts = time.time() - start_time + start_ts
ts_gaze_positions[ts] = gaze_position
- # Sleep a random time
- sleep_time = random.random() * (max_time - min_time) + min_time
- time.sleep(sleep_time)
-
return ts_gaze_positions
class TestVelocityThresholdIdentificationClass(unittest.TestCase):
@@ -119,16 +119,16 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase):
# Check result size
self.assertEqual(len(ts_fixations), 1)
self.assertEqual(len(ts_saccades), 0)
- self.assertEqual(len(ts_status), size-1)
+ self.assertEqual(len(ts_status), size - 1)
# Check fixation
ts, fixation = ts_fixations.pop_first()
- self.assertEqual(len(fixation.positions.keys()), size-1)
- self.assertGreaterEqual(fixation.duration, size * min_time)
- self.assertLessEqual(fixation.duration, size * max_time)
+ self.assertEqual(len(fixation.positions.keys()), size - 1)
+ self.assertGreaterEqual(fixation.duration, (size - 2) * min_time)
+ self.assertLessEqual(fixation.duration, (size - 2) * max_time)
self.assertLessEqual(fixation.finished, True)
-
+
def test_fixation_and_direct_saccade_identification(self):
"""Test VelocityThresholdIdentification fixation and saccade identification."""
@@ -138,7 +138,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase):
deviation_max = 10
min_time = 0.05
max_time = 0.1
- velocity_max = math.sqrt(2) * deviation_max / min_time
+ velocity_max = deviation_max / min_time
ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time)
ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_gaze_positions_A.last[0])
@@ -151,32 +151,46 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase):
# Check result size
self.assertEqual(len(ts_fixations), 2)
self.assertEqual(len(ts_saccades), 1)
- self.assertEqual(len(ts_status), size*2 - 1)
+ self.assertEqual(len(ts_status), size * 2 - 1)
# Check first fixation
ts, fixation = ts_fixations.pop_first()
- self.assertEqual(len(fixation.positions.keys()), size-1)
- self.assertGreaterEqual(fixation.duration, size * min_time)
- self.assertLessEqual(fixation.duration, size * max_time)
+ self.assertEqual(len(fixation.positions.keys()), size - 1)
+ self.assertGreaterEqual(fixation.duration, (size - 2) * min_time)
+ self.assertLessEqual(fixation.duration, (size - 2) * max_time)
self.assertLessEqual(fixation.finished, True)
# Check first saccade
ts, saccade = ts_saccades.pop_first()
- self.assertEqual(len(saccade.positions.keys()), 1)
- self.assertGreaterEqual(saccade.duration, 0.)
- self.assertLessEqual(saccade.duration, 0.)
+ self.assertEqual(len(saccade.positions.keys()), 2)
+ self.assertGreaterEqual(saccade.duration, min_time)
+ self.assertLessEqual(saccade.duration, max_time)
self.assertLessEqual(saccade.finished, True)
+ # Check that last position of a movement is equal to first position of next movement
+ last_ts, last_position = fixation.positions.last
+ first_ts, first_position = saccade.positions.first
+
+ self.assertEqual(last_ts, first_ts)
+ self.assertEqual(last_position.value, first_position.value)
+
# Check second fixation
ts, fixation = ts_fixations.pop_first()
- self.assertEqual(len(fixation.positions.keys()), size-1)
- self.assertGreaterEqual(fixation.duration, size * min_time)
- self.assertLessEqual(fixation.duration, size * max_time)
+ self.assertEqual(len(fixation.positions.keys()), size)
+ self.assertGreaterEqual(fixation.duration, (size - 1) * min_time)
+ self.assertLessEqual(fixation.duration, (size - 1) * max_time)
self.assertLessEqual(fixation.finished, True)
-
+
+ # Check that last position of a movement is equal to first position of next movement
+ last_ts, last_position = saccade.positions.last
+ first_ts, first_position = fixation.positions.first
+
+ self.assertEqual(last_ts, first_ts)
+ self.assertEqual(last_position.value, first_position.value)
+
def test_fixation_and_short_saccade_identification(self):
"""Test VelocityThresholdIdentification fixation and saccade identification."""
@@ -188,7 +202,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase):
deviation_max = 10
min_time = 0.05
max_time = 0.1
- velocity_max = math.sqrt(2) * deviation_max / min_time
+ velocity_max = deviation_max / min_time
ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time)
ts_move_positions = build_gaze_saccade(move, out_A, center_B, min_time, min_time, start_ts=ts_gaze_positions_A.last[0])
@@ -202,32 +216,46 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase):
# Check result size
self.assertEqual(len(ts_fixations), 2)
self.assertEqual(len(ts_saccades), 1)
- self.assertEqual(len(ts_status), 2 * size - 1 + move)
+ self.assertEqual(len(ts_status), 2 * size + move - 1)
# Check first fixation
ts, fixation = ts_fixations.pop_first()
- self.assertEqual(len(fixation.positions.keys()), size-1)
- self.assertGreaterEqual(fixation.duration, size * min_time)
- self.assertLessEqual(fixation.duration, size * max_time)
+ self.assertEqual(len(fixation.positions.keys()), size - 1)
+ self.assertGreaterEqual(fixation.duration, (size - 2) * min_time)
+ self.assertLessEqual(fixation.duration, (size - 2) * max_time)
self.assertLessEqual(fixation.finished, True)
# Check first saccade
ts, saccade = ts_saccades.pop_first()
- self.assertEqual(len(saccade.positions.keys()), move+1)
- self.assertGreaterEqual(saccade.duration, min_time)
- self.assertLessEqual(saccade.duration, max_time)
+ self.assertEqual(len(saccade.positions.keys()), move + 2)
+ self.assertGreaterEqual(saccade.duration, (move + 1) * min_time)
+ self.assertLessEqual(saccade.duration, (move + 1) * max_time)
self.assertLessEqual(saccade.finished, True)
+ # Check that last position of a movement is equal to first position of next movement
+ last_ts, last_position = fixation.positions.last
+ first_ts, first_position = saccade.positions.first
+
+ self.assertEqual(last_ts, first_ts)
+ self.assertEqual(last_position.value, first_position.value)
+
# Check second fixation
ts, fixation = ts_fixations.pop_first()
- self.assertEqual(len(fixation.positions.keys()), size-1)
- self.assertGreaterEqual(fixation.duration, size * min_time)
- self.assertLessEqual(fixation.duration, size * max_time)
+ self.assertEqual(len(fixation.positions.keys()), size)
+ self.assertGreaterEqual(fixation.duration, (size - 1) * min_time)
+ self.assertLessEqual(fixation.duration, (size - 1) * max_time)
self.assertLessEqual(fixation.finished, True)
+ # Check that last position of a movement is equal to first position of next movement
+ last_ts, last_position = saccade.positions.last
+ first_ts, first_position = fixation.positions.first
+
+ self.assertEqual(last_ts, first_ts)
+ self.assertEqual(last_position.value, first_position.value)
+
def test_invalid_gaze_position(self):
"""Test VelocityThresholdIdentification fixation and saccade identification with invalid gaze position."""
@@ -253,16 +281,16 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase):
ts, fixation = ts_fixations.pop_first()
self.assertEqual(len(fixation.positions.keys()), 6)
- self.assertGreaterEqual(fixation.duration, 6 * min_time)
- self.assertLessEqual(fixation.duration, 6 * max_time)
+ self.assertGreaterEqual(fixation.duration, 5 * min_time)
+ self.assertLessEqual(fixation.duration, 5 * max_time)
self.assertLessEqual(fixation.finished, True)
# Check second fixation
ts, fixation = ts_fixations.pop_first()
self.assertEqual(len(fixation.positions.keys()), 4)
- self.assertGreaterEqual(fixation.duration, 4 * min_time)
- self.assertLessEqual(fixation.duration, 4 * max_time)
+ self.assertGreaterEqual(fixation.duration, 3 * min_time)
+ self.assertLessEqual(fixation.duration, 3 * max_time)
self.assertLessEqual(fixation.finished, True)
if __name__ == '__main__':
diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
index 2b29492..4f79b7e 100644
--- a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
+++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
@@ -113,8 +113,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
"""Maximal velocity allowed to consider a gaze movement as a fixation."""
duration_min_threshold: int|float
- """Minimal duration allowed to consider a gaze movement as a fixation.
- It is also used as maximal duration allowed to consider a gaze movement as a saccade."""
+ """Minimal duration allowed to wait valid gaze positions."""
def __post_init__(self):
@@ -141,7 +140,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
self.__last_ts = ts
self.__last_position = gaze_position
- return
+ return GazeFeatures.UnvalidGazeMovement()
# Check if too much time elapsed since last gaze position
if (ts - self.__last_ts) > self.duration_min_threshold:
@@ -151,7 +150,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
self.__last_position = gaze_position
# Get last movement
- last_movement = self.current_saccade.finish() if len(self.__fixation_positions) == 0 else self.current_fixation.finish()
+ last_movement = self.current_gaze_movement.finish()
# Clear all former gaze positions
self.__fixation_positions = GazeFeatures.TimeStampedGazePositions()
@@ -170,49 +169,51 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
# Velocity is greater than threshold
if velocity > self.velocity_max_threshold:
- # Append to saccade positions
- self.__saccade_positions[ts] = gaze_position
+ last_fixation = GazeFeatures.UnvalidGazeMovement()
# Does last fixation exist?
if len(self.__fixation_positions) > 0:
+ # Copy most recent fixation position into saccade positions
+ last_ts, last_position = self.__fixation_positions.last
+ self.__saccade_positions[last_ts] = last_position
+
# Create last fixation
- last_fixation = Fixation(self.__fixation_positions).finish()
+ last_fixation = self.current_fixation.finish()
# Clear fixation positions
self.__fixation_positions = GazeFeatures.TimeStampedGazePositions()
- # Output last fixation
- return last_fixation
-
- # Identification must stop: ends with current saccade
- if terminate:
+ # Append to saccade positions
+ self.__saccade_positions[ts] = gaze_position
- return self.current_saccade.finish()
+ # Output last fixation
+ return last_fixation if not terminate else self.current_saccade.finish()
# Velocity is less or equals to threshold
else:
- # Append to fixation positions
- self.__fixation_positions[ts] = gaze_position
+ last_saccade = GazeFeatures.UnvalidGazeMovement()
# Does last saccade exist?
if len(self.__saccade_positions) > 0:
+ # Copy most recent saccade position into fixation positions
+ last_ts, last_position = self.__saccade_positions.last
+ self.__fixation_positions[last_ts] = last_position
+
# Create last saccade
- last_saccade = Saccade(self.__saccade_positions).finish()
+ last_saccade = self.current_saccade.finish()
# Clear fixation positions
self.__saccade_positions = GazeFeatures.TimeStampedGazePositions()
- # Output last saccade
- return last_saccade
-
- # Identification must stop: ends with current fixation
- if terminate:
-
- return self.current_fixation.finish()
+ # Append to fixation positions
+ self.__fixation_positions[ts] = gaze_position
+ # Output last saccade
+ return last_saccade if not terminate else self.current_fixation.finish()
+
# Always return unvalid gaze movement at least
return GazeFeatures.UnvalidGazeMovement()