From 40dab72aa46c253e76fb75a1228407d745e7a13a Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Tue, 23 May 2023 15:09:22 +0200 Subject: Adding and using entropy analysis. --- src/argaze/GazeAnalysis/Entropy.py | 58 ++++++++++++++++++++++++++++++ src/argaze/GazeAnalysis/__init__.py | 2 +- src/argaze/utils/demo_gaze_features_run.py | 30 ++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/argaze/GazeAnalysis/Entropy.py diff --git a/src/argaze/GazeAnalysis/Entropy.py b/src/argaze/GazeAnalysis/Entropy.py new file mode 100644 index 0000000..a185852 --- /dev/null +++ b/src/argaze/GazeAnalysis/Entropy.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +from typing import TypeVar, Tuple, Any +from dataclasses import dataclass, field + +from argaze import GazeFeatures + +import pandas +import numpy + +@dataclass +class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer): + """Implementation of entropy algorithm as described in Krejtz et al., 2014 + """ + + def __post_init__(self): + + pass + + def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPathType, transition_matrix_probability: pandas.DataFrame) -> Any: + """Analyze aoi scan.""" + + assert(len(aoi_scan_path) > 1) + + # Count total number of fixations and how many fixations are there per aoi + scan_fixation_count, aoi_fixation_count = aoi_scan_path.fixation_count() + + # Probability to have a fixation onto each aoi + stationary_probalities = {aoi: count/scan_fixation_count for aoi, count in aoi_fixation_count.items()} + + # Stationary entropy + stationary_entropy = 0 + + for aoi, p in stationary_probalities.items(): + + stationary_entropy += p * numpy.log(p + 0.00000001) + + stationary_entropy *= -1 + + # Transition entropy + transition_entropy = 0 + + aoi_sum = transition_matrix_probability.apply(lambda row: row.apply(lambda p: p * numpy.log(p + 0.00000001)).sum(), axis=1) + + for aoi, s in aoi_sum.items(): + + transition_entropy += s * stationary_probalities[aoi] + + transition_entropy *= -1 + + return stationary_entropy, transition_entropy diff --git a/src/argaze/GazeAnalysis/__init__.py b/src/argaze/GazeAnalysis/__init__.py index d82969d..cb18a08 100644 --- a/src/argaze/GazeAnalysis/__init__.py +++ b/src/argaze/GazeAnalysis/__init__.py @@ -2,4 +2,4 @@ .. include:: README.md """ __docformat__ = "restructuredtext" -__all__ = ['DispersionThresholdIdentification', 'VelocityThresholdIdentification', 'TransitionMatrix', 'CoefficientK', 'LempelZivComplexity', 'NGram'] \ No newline at end of file +__all__ = ['DispersionThresholdIdentification', 'VelocityThresholdIdentification', 'TransitionMatrix', 'CoefficientK', 'LempelZivComplexity', 'NGram', 'Entropy'] \ No newline at end of file diff --git a/src/argaze/utils/demo_gaze_features_run.py b/src/argaze/utils/demo_gaze_features_run.py index 6384205..736ffa9 100644 --- a/src/argaze/utils/demo_gaze_features_run.py +++ b/src/argaze/utils/demo_gaze_features_run.py @@ -102,6 +102,10 @@ def main(): ngram_analysis = {} enable_ngram_analysis = False + entropy_analyzer = Entropy.AOIScanPathAnalyzer() + entropy_analysis = (-1, -1) + enable_entropy_analysis = False + gaze_movement_lock = threading.Lock() # Init timestamp @@ -121,6 +125,7 @@ def main(): nonlocal aoi_cK_analysis nonlocal lzc_analysis nonlocal ngram_analysis + nonlocal entropy_analysis # Edit millisecond timestamp data_ts = int((time.time() - start_ts) * 1e3) @@ -205,6 +210,10 @@ def main(): ngram_analysis = ngram_analyzer.analyze(aoi_scan_path, 3) + if enable_entropy_analysis and enable_tm_analysis: + + entropy_analysis = entropy_analyzer.analyze(aoi_scan_path, tm_probabilities) + except GazeFeatures.AOIScanStepError as e: print(f'Error on {e.aoi} step:', e) @@ -280,6 +289,11 @@ def main(): display_hide = 'hide' if enable_ngram_analysis else 'display' cv2.putText(aoi_matrix, f'Tri-Gram: {on_off} (Press \'n\' key to {display_hide})', (20, 240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255) if enable_ngram_analysis else (255, 255, 255), 1, cv2.LINE_AA) + # Write entropy help + on_off = 'on' if enable_entropy_analysis else 'off' + display_hide = 'hide' if enable_entropy_analysis else 'display' + cv2.putText(aoi_matrix, f'Entropy: {on_off} (Press \'e\' key to {display_hide})', (20, 280), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255) if enable_entropy_analysis else (255, 255, 255), 1, cv2.LINE_AA) + # Check fixation identification if gaze_movement_identifier[identification_mode].current_fixation != None: @@ -378,6 +392,11 @@ def main(): trigram = f'{ngram[0]}>{ngram[1]}>{ngram[2]}' cv2.putText(aoi_matrix, f'{trigram}: {count}', (window_size[0]-700, start+(i*40)), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) + + # 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) # Unlock gaze movement identification gaze_movement_lock.release() @@ -430,6 +449,17 @@ def main(): enable_ngram_analysis = not enable_ngram_analysis + # Enable entropy analysis with 'e' key + if key_pressed == 101: + + enable_entropy_analysis = not enable_entropy_analysis + + # Transition matrix is needed + if enable_entropy_analysis: + + enable_tm_analysis = True + + # Stop calibration by pressing 'Esc' key if cv2.waitKey(10) == 27: break -- cgit v1.1