aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/argaze/GazeAnalysis/TransitionMatrix.py14
-rw-r--r--src/argaze/GazeFeatures.py30
-rw-r--r--src/argaze/utils/demo_gaze_features_run.py11
3 files changed, 43 insertions, 12 deletions
diff --git a/src/argaze/GazeAnalysis/TransitionMatrix.py b/src/argaze/GazeAnalysis/TransitionMatrix.py
index 6fd7a7b..dce61de 100644
--- a/src/argaze/GazeAnalysis/TransitionMatrix.py
+++ b/src/argaze/GazeAnalysis/TransitionMatrix.py
@@ -13,10 +13,11 @@ from dataclasses import dataclass, field
from argaze import GazeFeatures
import pandas
+import numpy
@dataclass
class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer):
- """Implementation of transition matrix probabilities and density algorithm as described in ...
+ """Implementation of transition matrix probabilities and density algorithm as described in Krejtz et al., 2014
"""
def __post_init__(self):
@@ -28,13 +29,14 @@ class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer):
assert(len(aoi_scan_path) > 1)
- sequence = []
+ # Sum transitions starting from each aoi
+ row_sum = aoi_scan_path.transition_matrix.apply(lambda row: row.sum(), axis=1)
- for aoi_scan_step in aoi_scan_path:
+ # Editing transition matrix probabilities
+ # Note: when no transiton starts from an aoi, destination probabilites is equal to 1/S where S is the number of aois
+ transition_matrix_probabilities = aoi_scan_path.transition_matrix.apply(lambda row: row.apply(lambda p: p / row_sum[row.name] if row_sum[row.name] > 0 else 1 / row_sum.size), axis=1)
- sequence.append(aoi_scan_step.aoi)
-
- transition_matrix_probabilities = pandas.crosstab(pandas.Series(sequence[1:], name='to'), pandas.Series(sequence[:-1], name='from'), normalize='index')
+ # Calculate matrix density
transition_matrix_density = (transition_matrix_probabilities != 0.).astype(int).sum(axis=1).sum() / transition_matrix_probabilities.size
return transition_matrix_probabilities, transition_matrix_density
diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py
index a098dc4..21841ac 100644
--- a/src/argaze/GazeFeatures.py
+++ b/src/argaze/GazeFeatures.py
@@ -548,18 +548,22 @@ AOIScanPathType = TypeVar('AOIScanPathType', bound="AOIScanPathType")
# Type definition for type annotation convenience
class AOIScanPath(list):
- """List of aoi scan steps over successive AOI."""
+ """List of aoi scan steps over successive aoi."""
- def __init__(self):
+ def __init__(self, expected_aois: list[str] = []):
super().__init__()
+ self.__expected_aois = expected_aois
self.__movements = TimeStampedGazeMovements()
self.__current_aoi = ''
self.__index = ord('A')
self.__aoi_letter = {}
self.__letter_aoi = {}
+ size = len(self.__expected_aois)
+ self.__transition_matrix = pandas.DataFrame(numpy.zeros((size, size)), index=self.__expected_aois, columns=self.__expected_aois)
+
def __repr__(self):
"""String representation."""
@@ -593,11 +597,23 @@ class AOIScanPath(list):
return sequence
@property
+ def expected_aois(self):
+ """List of all expected aoi."""
+
+ return self.__expected_aois
+
+ @property
def current_aoi(self):
"""AOI name of aoi scan step under construction"""
return self.__current_aoi
+ @property
+ def transition_matrix(self) -> pandas.DataFrame:
+ """Pandas DataFrame where indexes are transition departures and columns are transition destinations."""
+
+ return self.__transition_matrix
+
def append_saccade(self, ts, saccade):
"""Append new saccade to aoi scan path."""
@@ -612,6 +628,10 @@ class AOIScanPath(list):
.. warning::
It could raise AOIScanStepError"""
+ if looked_aoi not in self.__expected_aois:
+
+ raise AOIScanStepError('AOI not expected', looked_aoi)
+
# Is it fixation onto a new aoi?
if looked_aoi != self.__current_aoi and len(self.__movements) > 0:
@@ -626,6 +646,12 @@ class AOIScanPath(list):
# Edit new step
new_step = AOIScanStep(self.__movements, self.__current_aoi, letter)
+ # Edit transition matrix
+ if len(self) > 0:
+
+ # Increment [index: source, columns: destination] value
+ self.__transition_matrix.loc[self[-1].aoi, self.__current_aoi,] += 1
+
# Append new step
super().append(new_step)
diff --git a/src/argaze/utils/demo_gaze_features_run.py b/src/argaze/utils/demo_gaze_features_run.py
index 736ffa9..50cf113 100644
--- a/src/argaze/utils/demo_gaze_features_run.py
+++ b/src/argaze/utils/demo_gaze_features_run.py
@@ -80,7 +80,7 @@ def main():
identification_mode = 'I-DT'
raw_scan_path = GazeFeatures.ScanPath()
- aoi_scan_path = GazeFeatures.AOIScanPath()
+ aoi_scan_path = GazeFeatures.AOIScanPath(aoi_scene_projection.keys())
tm = TransitionMatrix.AOIScanPathAnalyzer()
tm_probabilities = pandas.DataFrame()
@@ -342,9 +342,11 @@ def main():
cv2.putText(aoi_matrix, f'Transition matrix density: {tm_density:.2f}', (20, window_size[1]-160), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA)
- for from_aoi, column in tm_probabilities.items():
+ # Iterate over indexes (departures)
+ for from_aoi, row in tm_probabilities.iterrows():
- for to_aoi, probability in column.items():
+ # Iterate over columns (destinations)
+ for to_aoi, probability in row.items():
if from_aoi != to_aoi and probability > 0.0:
@@ -396,7 +398,8 @@ def main():
# Write entropy
if enable_entropy_analysis:
- cv2.putText(aoi_matrix, f'Entropy: {entropy_analysis}', (20, window_size[1]-240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA)
+ cv2.putText(aoi_matrix, f'Stationary entropy: {entropy_analysis[0]:.3f},', (20, window_size[1]-280), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA)
+ cv2.putText(aoi_matrix, f'Transition entropy: {entropy_analysis[1]:.3f},', (20, window_size[1]-240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA)
# Unlock gaze movement identification
gaze_movement_lock.release()