aboutsummaryrefslogtreecommitdiff
path: root/src/argaze/GazeAnalysis/LinearRegression.py
blob: 5a823a1ee0c8f1a3a7770c9228b1b081e581c403 (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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""Module for gaze position calibration based on linear regression.


This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""

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

from argaze import DataFeatures, GazeFeatures

from sklearn.linear_model import LinearRegression
import numpy
import cv2

class GazePositionCalibrator(GazeFeatures.GazePositionCalibrator):
    """Implementation of linear regression algorithm as described in:

        **Drewes, H., Pfeuffer, K., & Alt, F. (2019, June).**  
        *Time- and space-efficient eye tracker calibration.*  
        Proceedings of the 11th ACM Symposium on Eye Tracking Research & Applications (ETRA'19, 1-8).  
        [https://dl.acm.org/doi/pdf/10.1145/3314111.3319818](https://dl.acm.org/doi/pdf/10.1145/3314111.3319818)
    """
    @DataFeatures.PipelineStepInit
    def __init__(self, **kwargs):

        # Init GazePositionCalibrator class
        super().__init__()

        self.__linear_regression = LinearRegression()
        self.__linear_regression.coef_ = numpy.array([[1., 0.], [0., 1.]])
        self.__linear_regression.intercept_ = numpy.array([0., 0.])

    @property
    def coefficients(self) -> list:
        """Linear regression coefficients."""
        return self.__linear_regression.coef_.tolist()

    @coefficients.setter
    def coefficients(self, coefficients: list):

        self.__linear_regression.coef_ = numpy.array(coefficients)

    @property
    def intercept(self) -> list:
        """Linear regression intercept value."""
        return self.__linear_regression.intercept_.tolist()

    @intercept.setter 
    def intercept(self, intercept: list):

        self.__linear_regression.intercept_ = numpy.array(intercept)

    def is_calibrating(self) -> bool:
        """Is the calibration running?"""
        return self.__linear_regression is None

    def store(self, observed_gaze_position: GazeFeatures.GazePosition, expected_gaze_position: GazeFeatures.GazePosition):
        """Store observed and expected gaze positions."""
        self.__observed_positions.append(observed_gaze_position)
        self.__expected_positions.append(expected_gaze_position)

    def reset(self):
        """Reset observed and expected gaze positions."""
        # noinspection PyAttributeOutsideInit
        self.__observed_positions = []
        # noinspection PyAttributeOutsideInit
        self.__expected_positions = []
        self.__linear_regression = None

    def calibrate(self) -> float:
        """Process calibration from observed and expected gaze positions.

        Returns:
            score: the score of linear regression
        """
        self.__linear_regression = LinearRegression().fit(self.__observed_positions, self.__expected_positions)

        # Return calibrated gaze position
        return self.__linear_regression.score(self.__observed_positions, self.__expected_positions)

    def apply(self, gaze_position: GazeFeatures.GazePosition) -> GazeFeatures.GazePosition:
        """Apply calibration onto observed gaze position."""
        if not self.is_calibrating():

            return GazeFeatures.GazePosition(self.__linear_regression.predict(numpy.array([gaze_position]))[0], precision=gaze_position.precision, timestamp=gaze_position.timestamp)

        else:

            return gaze_position

    def draw(self, image: numpy.array, size: tuple, resolution: tuple, line_color: tuple = (0, 0, 0), thickness: int = 1):
        """Draw calibration field."""
        width, height = size
        
        if width * height > 0:

            rx, ry = resolution
            lx = numpy.linspace(0, width, rx)
            ly = numpy.linspace(0, height, ry)
            xv, yv = numpy.meshgrid(lx, ly, indexing='ij')

            for i in range(rx):

                for j in range(ry):

                    start = (xv[i][j], yv[i][j])
                    end = self.apply(GazeFeatures.GazePosition(start)).value

                    cv2.line(image, (int(start[0]), int(start[1])), (int(end[0]), int(end[1])), line_color, thickness)