aboutsummaryrefslogtreecommitdiff
path: root/src/argaze
diff options
context:
space:
mode:
authorThéo de la Hogue2024-02-29 11:09:32 +0100
committerThéo de la Hogue2024-02-29 11:09:32 +0100
commit1a3aac125980019ae86493782795569327bc8eaa (patch)
tree0ae2c4a9fb57e51b0a510f63b9503648f41642d3 /src/argaze
parentfaa6d8acf3c9e4d11a3ee84df2d5a48501befd68 (diff)
downloadargaze-1a3aac125980019ae86493782795569327bc8eaa.zip
argaze-1a3aac125980019ae86493782795569327bc8eaa.tar.gz
argaze-1a3aac125980019ae86493782795569327bc8eaa.tar.bz2
argaze-1a3aac125980019ae86493782795569327bc8eaa.tar.xz
Fixing VelocityThresholdIdentification tests.
Diffstat (limited to 'src/argaze')
-rw-r--r--src/argaze/GazeAnalysis/DispersionThresholdIdentification.py18
-rw-r--r--src/argaze/GazeAnalysis/VelocityThresholdIdentification.py129
2 files changed, 71 insertions, 76 deletions
diff --git a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py
index c85e576..13529e7 100644
--- a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py
+++ b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py
@@ -100,8 +100,8 @@ class Saccade(GazeFeatures.Saccade):
# Draw line if required
if line_color is not None:
- _, start_position = self.positions[0]
- _, last_position = self.positions[-1]
+ start_position = self.positions[0]
+ last_position = self.positions[-1]
cv2.line(image, (int(start_position[0]), int(start_position[1])), (int(last_position[0]), int(last_position[1])), line_color, 2)
@@ -143,13 +143,13 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
@DataFeatures.PipelineStepMethod
def identify(self, timestamp: int|float, gaze_position, terminate=False) -> GazeMovementType:
- # Ignore non valid gaze position
+ # Ignore empty gaze position
if not gaze_position:
return GazeFeatures.GazeMovement() if not terminate else self.current_fixation.finish()
# Check if too much time elapsed since last valid gaze position
- if len(self.__valid_positions) > 0:
+ if self.__valid_positions:
ts_last = self.__valid_positions[-1].timestamp
@@ -184,7 +184,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
last_saccade = GazeFeatures.GazeMovement()
# Is there saccade positions?
- if len(self.__saccade_positions) > 0:
+ if self.__saccade_positions:
# Copy oldest valid position into saccade positions
self.__saccade_positions.append(self.__valid_positions[0])
@@ -207,7 +207,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
last_fixation = GazeFeatures.GazeMovement()
# Is there fixation positions?
- if len(self.__fixation_positions) > 0:
+ if self.__fixation_positions:
# Copy most recent fixation position into saccade positions
self.__saccade_positions.append(self.__fixation_positions[-1])
@@ -237,9 +237,9 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
def current_gaze_movement(self) -> GazeMovementType:
# It shouldn't have a current fixation and a current saccade at the same time
- assert(not (len(self.__fixation_positions) > 0 and len(self.__saccade_positions) > 0))
+ assert(not (self.__fixation_positions and len(self.__saccade_positions) > 1))
- if len(self.__fixation_positions) > 0:
+ if self.__fixation_positions:
return Fixation(self.__fixation_positions)
@@ -252,7 +252,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
@property
def current_fixation(self) -> FixationType:
- if len(self.__fixation_positions) > 0:
+ if self.__fixation_positions:
return Fixation(self.__fixation_positions)
diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
index 971ba9b..c1d448a 100644
--- a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
+++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
@@ -25,53 +25,39 @@ FixationType = TypeVar('Fixation', bound="Fixation")
SaccadeType = TypeVar('Saccade', bound="Saccade")
# Type definition for type annotation convenience
-@dataclass(frozen=True)
class Fixation(GazeFeatures.Fixation):
"""Define dispersion based fixation."""
- deviation_max: float = field(init=False)
- """Maximal gaze position distance to the centroïd."""
+ def __init__(self, positions: GazeFeatures.TimeStampedGazePositions = (), finished: bool = False, message: str = None, **kwargs):
- def __post_init__(self):
+ super().__init__(positions, finished, message, **kwargs)
- super().__post_init__()
+ if positions:
- points = self.positions.values()
- points_x, points_y = [p[0] for p in points], [p[1] for p in points]
- points_array = numpy.column_stack([points_x, points_y])
- centroid_array = numpy.array([numpy.mean(points_x), numpy.mean(points_y)])
- deviations_array = numpy.sqrt(numpy.sum((points_array - centroid_array)**2, axis=1))
+ positions_array = numpy.asarray(self.values())
+ centroid = numpy.mean(positions_array, axis=0)
+ deviations_array = numpy.sqrt(numpy.sum((positions_array - centroid)**2, axis=1))
- # Update frozen focus attribute using centroid
- object.__setattr__(self, 'focus', (centroid_array[0], centroid_array[1]))
+ # Set focus as positions centroid
+ self.focus = (centroid[0], centroid[1])
- # Update frozen deviation_max attribute
- object.__setattr__(self, 'deviation_max', max(deviations_array))
+ # Set deviation_max attribute
+ self.__deviation_max = deviations_array.max()
- def point_deviation(self, gaze_position) -> float:
- """Get distance of a point from the fixation's centroïd."""
-
- return numpy.sqrt((self.centroid[0] - gaze_position.value[0])**2 + (self.centroid[1] - gaze_position.value[1])**2)
+ @property
+ def deviation_max(self):
+ """Get fixation's maximal deviation."""
+ return self.__deviation_max
- def overlap(self, fixation) -> bool:
+ def overlap(self, fixation: FixationType) -> bool:
"""Does a gaze position from another fixation having a deviation to this fixation centroïd smaller than maximal deviation?"""
-
- points = fixation.positions.values()
- points_x, points_y = [p[0] for p in points], [p[1] for p in points]
- points_array = numpy.column_stack([points_x, points_y])
- centroid_array = numpy.array([self.centroid[0], self.centroid[1]])
- deviations_array = numpy.sqrt(numpy.sum((points_array - centroid_array)**2, axis=1))
+
+ positions_array = numpy.asarray(fixation.values())
+ centroid = numpy.mean(self.focus, axis=0)
+ deviations_array = numpy.sqrt(numpy.sum((positions_array - centroid)**2, axis=1))
return min(deviations_array) <= self.deviation_max
-
- def merge(self, fixation) -> FixationType:
- """Merge another fixation into this fixation."""
-
- self.positions.append(fixation.positions)
- self.__post_init__()
-
- return self
-
+
def draw(self, image: numpy.array, deviation_circle_color: tuple = None, duration_border_color: tuple = None, duration_factor: float = 1., draw_positions: dict = None):
"""Draw fixation into image.
@@ -85,7 +71,7 @@ class Fixation(GazeFeatures.Fixation):
if duration_border_color is not None:
cv2.circle(image, (int(self.focus[0]), int(self.focus[1])), int(self.deviation_max), duration_border_color, int(self.duration * duration_factor))
-
+
# Draw deviation circle if required
if deviation_circle_color is not None:
@@ -96,12 +82,12 @@ class Fixation(GazeFeatures.Fixation):
self.draw_positions(image, **draw_positions)
-@dataclass(frozen=True)
class Saccade(GazeFeatures.Saccade):
"""Define dispersion based saccade."""
- def __post_init__(self):
- super().__post_init__()
+ def __init__(self, positions: GazeFeatures.TimeStampedGazePositions = (), finished: bool = False, message: str = None, **kwargs):
+
+ super().__init__(positions, finished, message, **kwargs)
def draw(self, image: numpy.array, line_color: tuple = None):
"""Draw saccade into image.
@@ -113,8 +99,8 @@ class Saccade(GazeFeatures.Saccade):
# Draw line if required
if line_color is not None:
- _, start_position = self.positions.first
- _, last_position = self.positions.last
+ start_position = self.positions[0]
+ last_position = self.positions[-1]
cv2.line(image, (int(start_position[0]), int(start_position[1])), (int(last_position[0]), int(last_position[1])), line_color, 2)
@@ -126,45 +112,56 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
saccades in eye-tracking protocols. In Proceedings of the 2000 symposium
on Eye tracking research & applications (ETRA '00). ACM, New York, NY, USA,
71-78. [http://dx.doi.org/10.1145/355017.355028](http://dx.doi.org/10.1145/355017.355028)
- """
-
- velocity_max_threshold: int|float
- """Maximal velocity allowed to consider a gaze movement as a fixation."""
- duration_min_threshold: int|float
- """Minimal duration allowed to wait valid gaze positions."""
+ Parameters:
+ velocity_max_threshold: Maximal velocity allowed to consider a gaze movement as a fixation.
+ duration_min_threshold: Minimal duration allowed to wait valid gaze positions.
+ """
- def __post_init__(self):
+ def __init__(self, velocity_max_threshold: int|float, duration_min_threshold: int|float):
super().__init__()
+ self.__velocity_max_threshold = velocity_max_threshold
+ self.__duration_min_threshold = duration_min_threshold
+
self.__last_ts = -1
self.__last_position = None
self.__fixation_positions = GazeFeatures.TimeStampedGazePositions()
self.__saccade_positions = GazeFeatures.TimeStampedGazePositions()
+ @property
+ def velocity_max_threshold(self):
+ """Get identifier's velocity max threshold."""
+ return self.__velocity_max_threshold
+
+ @property
+ def duration_min_threshold(self):
+ """Get identifier duration min threshold."""
+ return self.__duration_min_threshold
+
@DataFeatures.PipelineStepMethod
- def identify(self, ts: int|float, gaze_position, terminate=False) -> GazeMovementType:
+ def identify(self, timestamp: int|float, gaze_position, terminate=False) -> GazeMovementType:
- # Ignore non valid gaze position
- if not gaze_position.valid:
+ # Ignore empty gaze position
+ if not gaze_position:
return GazeFeatures.GazeMovement() if not terminate else self.current_fixation.finish()
# Store first valid position
if self.__last_ts < 0:
- self.__last_ts = ts
+ self.__last_ts = timestamp
self.__last_position = gaze_position
return GazeFeatures.GazeMovement()
# Check if too much time elapsed since last gaze position
- if (ts - self.__last_ts) > self.duration_min_threshold:
+ if (timestamp - self.__last_ts) > self.duration_min_threshold:
# Remember last position
- self.__last_ts = ts
+ self.__last_ts = timestamp
self.__last_position = gaze_position
# Get last movement
@@ -178,10 +175,10 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
return last_movement
# Velocity
- velocity = abs(gaze_position.distance(self.__last_position) / (ts - self.__last_ts))
+ velocity = abs(gaze_position.distance(self.__last_position) / (timestamp - self.__last_ts))
# Remember last position
- self.__last_ts = ts
+ self.__last_ts = timestamp
self.__last_position = gaze_position
# Velocity is greater than threshold
@@ -193,8 +190,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
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
+ self.__saccade_positions.append(self.__fixation_positions[-1])
# Create last fixation
last_fixation = self.current_fixation.finish()
@@ -203,7 +199,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
self.__fixation_positions = GazeFeatures.TimeStampedGazePositions()
# Append to saccade positions
- self.__saccade_positions[ts] = gaze_position
+ self.__saccade_positions.append(gaze_position)
# Output last fixation
return last_fixation if not terminate else self.current_saccade.finish()
@@ -214,11 +210,10 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
last_saccade = GazeFeatures.GazeMovement()
# Does last saccade exist?
- if len(self.__saccade_positions) > 0:
+ if self.__saccade_positions:
# Copy most recent saccade position into fixation positions
- last_ts, last_position = self.__saccade_positions.last
- self.__fixation_positions[last_ts] = last_position
+ self.__fixation_positions.append(self.__saccade_positions[-1])
# Create last saccade
last_saccade = self.current_saccade.finish()
@@ -227,7 +222,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
self.__saccade_positions = GazeFeatures.TimeStampedGazePositions()
# Append to fixation positions
- self.__fixation_positions[ts] = gaze_position
+ self.__fixation_positions.append(gaze_position)
# Output last saccade
return last_saccade if not terminate else self.current_fixation.finish()
@@ -239,13 +234,13 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
def current_gaze_movement(self) -> GazeMovementType:
# It shouldn't have a current fixation and a current saccade at the same time
- assert(not (len(self.__fixation_positions) > 0 and len(self.__saccade_positions) > 0))
+ assert(not (self.__fixation_positions and self.__saccade_positions))
- if len(self.__fixation_positions) > 0:
+ if self.__fixation_positions:
return Fixation(self.__fixation_positions)
- if len(self.__saccade_positions) > 0:
+ if len(self.__saccade_positions) > 1:
return Saccade(self.__saccade_positions)
@@ -255,7 +250,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
@property
def current_fixation(self) -> FixationType:
- if len(self.__fixation_positions) > 0:
+ if self.__fixation_positions:
return Fixation(self.__fixation_positions)
@@ -265,7 +260,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
@property
def current_saccade(self) -> SaccadeType:
- if len(self.__saccade_positions) > 0:
+ if len(self.__saccade_positions) > 1:
return Saccade(self.__saccade_positions)