aboutsummaryrefslogtreecommitdiff
path: root/src/argaze/GazeAnalysis/DeviationCircleCoverage.py
blob: bde486dca347894e9a2525b75021e1d2a323e655 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/usr/bin/env python

"""Module for matching algorithm based on fixation's deviation circle coverage over AOI.
"""

__author__ = "Théo de la Hogue"
__credits__ = []
__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)"
__license__ = "BSD"

from typing import TypeVar, Tuple
from dataclasses import dataclass, field
import math

from argaze import GazeFeatures

import numpy
import cv2

GazeMovementType = TypeVar('GazeMovement', bound="GazeMovement")
# Type definition for type annotation convenience

@dataclass
class AOIMatcher(GazeFeatures.AOIMatcher):
    """Matching algorithm based on fixation's deviation circle coverage over AOI."""

    coverage_threshold: float
    """Minimal coverage ratio to consider a fixation over an AOI (1 means that whole fixation's deviation circle have to be over the AOI)."""

    def __post_init__(self):
        """Init looked aoi data."""

        self.__look_count = 0
        self.__looked_aoi = None
        self.__looked_aoi_coverage_mean = 0
        self.__looked_aoi_coverage = {}

    def match(self, aoi_scene, gaze_movement, exclude=[]) -> str:
        """Returns AOI with the maximal fixation's deviation circle coverage if above coverage threshold."""

        if GazeFeatures.is_fixation(gaze_movement):

            self.__look_count += 1

            max_coverage = 0.
            most_likely_looked_aoi = None

            for name, aoi in aoi_scene.items():

                _, _, circle_ratio = aoi.circle_intersection(gaze_movement.focus, gaze_movement.deviation_max)

                if name not in exclude and circle_ratio > 0:

                    # Sum circle ratio to update aoi coverage
                    try:

                        self.__looked_aoi_coverage[name] += circle_ratio

                    except KeyError:

                        self.__looked_aoi_coverage[name] = circle_ratio

                    # Update most likely looked aoi
                    if self.__looked_aoi_coverage[name] > max_coverage:

                        most_likely_looked_aoi = name
                        max_coverage = self.__looked_aoi_coverage[name]

            # Update looked aoi
            self.__looked_aoi = most_likely_looked_aoi

            # Update looked aoi coverage mean
            self.__looked_aoi_coverage_mean = int(100 * max_coverage / self.__look_count) / 100

            # Return
            if self.looked_aoi_coverage_mean > self.coverage_threshold:

                return self.__looked_aoi

        elif GazeFeatures.is_saccade(gaze_movement):

            self.__post_init__()

    @property
    def looked_aoi(self) -> str:
        """Get most likely looked aoi name for current fixation (e.g. the aoi with the highest coverage mean value)"""

        return self.__looked_aoi

    @property
    def looked_aoi_coverage_mean(self) -> float:
        """Get looked aoi coverage mean for current fixation. 
        It represents the ratio of fixation deviation circle surface that used to cover the looked aoi."""

        return self.__looked_aoi_coverage_mean

    @property
    def looked_aoi_coverage(self) -> dict:
        """Get all looked aois coverage for current fixation."""

        return self.__looked_aoi_coverage