#!/usr/bin/env python """Nearest Neighbor Index module. """ __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 algorithm as described in: **Di Nocera F., Terenzi M., Camilli M. (2006).** *Another look at scanpath: distance to nearest neighbour as a measure of mental workload.* Developments in Human Factors in Transportation, Design, and Evaluation. [https://www.researchgate.net](https://www.researchgate.net/publication/239470608_Another_look_at_scanpath_distance_to_nearest_neighbour_as_a_measure_of_mental_workload) """ size: tuple[float, float] """Frame dimension.""" def __post_init__(self): super().__init__() self.__nearest_neighbor_index = 0 def analyze(self, scan_path: GazeFeatures.ScanPathType): """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(self.size[0] * self.size[1] / len(fixations_focus)) self.__nearest_neighbor_index = dNN / dran @property def nearest_neighbor_index(self) -> float: """Nearest Neighbor Index.""" return self.__nearest_neighbor_index