aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/argaze/GazeAnalysis/NearestNeighborIndex.py49
-rw-r--r--src/argaze/GazeAnalysis/__init__.py2
-rw-r--r--src/argaze/utils/demo_gaze_features_run.py25
3 files changed, 74 insertions, 2 deletions
diff --git a/src/argaze/GazeAnalysis/NearestNeighborIndex.py b/src/argaze/GazeAnalysis/NearestNeighborIndex.py
new file mode 100644
index 0000000..104bb30
--- /dev/null
+++ b/src/argaze/GazeAnalysis/NearestNeighborIndex.py
@@ -0,0 +1,49 @@
+#!/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 numpy
+from scipy.spatial.distance import cdist
+
+@dataclass
+class ScanPathAnalyzer(GazeFeatures.ScanPathAnalyzer):
+ """Implementation of Nearest Neighbor Index (NNI) as described in Di Nocera et al., 2006
+ """
+
+ def __post_init__(self):
+
+ pass
+
+ def analyze(self, scan_path: GazeFeatures.ScanPathType, screen_dimension: tuple[float, float]) -> float:
+ """Analyze scan path."""
+
+ assert(len(scan_path) > 1)
+
+ # Gather fixations focus points
+ fixations_focus = []
+ for step in scan_path:
+ fixations_focus.append(step.first_fixation.focus)
+
+ # Compute inter fixation distances
+ distances = cdist(fixations_focus, fixations_focus)
+
+ # Find minimal distances between each fixations
+ minimums = numpy.apply_along_axis(lambda row: numpy.min(row[numpy.nonzero(row)]), 1, distances)
+
+ # Average of minimun distances
+ dNN = numpy.sum(minimums / len(fixations_focus))
+
+ # Mean random distance
+ dran = 0.5 * numpy.sqrt(screen_dimension[0] * screen_dimension[1] / len(fixations_focus))
+
+ return dNN / dran
diff --git a/src/argaze/GazeAnalysis/__init__.py b/src/argaze/GazeAnalysis/__init__.py
index cb18a08..f955856 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', 'Entropy'] \ No newline at end of file
+__all__ = ['DispersionThresholdIdentification', 'VelocityThresholdIdentification', 'TransitionMatrix', 'CoefficientK', 'LempelZivComplexity', 'NGram', 'Entropy', 'NearestNeighborIndex'] \ 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 50cf113..0e247e2 100644
--- a/src/argaze/utils/demo_gaze_features_run.py
+++ b/src/argaze/utils/demo_gaze_features_run.py
@@ -46,7 +46,7 @@ def main():
aoi_scene_filepath = os.path.join(current_directory, 'demo_environment/aoi_scene.jpg')
aoi_scene_image = cv2.imread(aoi_scene_filepath)
- window_size = [aoi_scene_image.shape[1], aoi_scene_image.shape[0]]
+ window_size = (aoi_scene_image.shape[1], aoi_scene_image.shape[0])
# Project AOI scene onto Full HD screen
aoi_scene_projection = demo_scene.orthogonal_projection * window_size
@@ -106,6 +106,10 @@ def main():
entropy_analysis = (-1, -1)
enable_entropy_analysis = False
+ nni_analyzer = NearestNeighborIndex.ScanPathAnalyzer()
+ nni_analysis = 0
+ enable_nni_analysis = False
+
gaze_movement_lock = threading.Lock()
# Init timestamp
@@ -126,6 +130,7 @@ def main():
nonlocal lzc_analysis
nonlocal ngram_analysis
nonlocal entropy_analysis
+ nonlocal nni_analysis
# Edit millisecond timestamp
data_ts = int((time.time() - start_ts) * 1e3)
@@ -230,6 +235,10 @@ def main():
raw_cK_analysis = raw_cK_analyzer.analyze(raw_scan_path)
+ if enable_nni_analysis:
+
+ nni_analysis = nni_analyzer.analyze(raw_scan_path, window_size)
+
# Append saccade to aoi scan path
aoi_scan_path.append_saccade(data_ts, gaze_movement)
@@ -294,6 +303,11 @@ def main():
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)
+ # Write nni help
+ on_off = 'on' if enable_nni_analysis else 'off'
+ display_hide = 'hide' if enable_nni_analysis else 'display'
+ cv2.putText(aoi_matrix, f'Nearest neighbor index: {on_off} (Press \'i\' key to {display_hide})', (20, 320), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255) if enable_nni_analysis else (255, 255, 255), 1, cv2.LINE_AA)
+
# Check fixation identification
if gaze_movement_identifier[identification_mode].current_fixation != None:
@@ -401,6 +415,11 @@ def main():
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)
+ # Write NNI
+ if enable_nni_analysis:
+
+ cv2.putText(aoi_matrix, f'Nearest neighbor index: {nni_analysis:.3f}', (20, window_size[1]-320), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA)
+
# Unlock gaze movement identification
gaze_movement_lock.release()
@@ -462,6 +481,10 @@ def main():
enable_tm_analysis = True
+ # Enable NNI analysis with 'i' key
+ if key_pressed == 105:
+
+ enable_nni_analysis = not enable_nni_analysis
# Stop calibration by pressing 'Esc' key
if cv2.waitKey(10) == 27: