remote-calibrator 0.3.0 → 0.5.0-beta.3
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +12 -0
- package/README.md +29 -19
- package/homepage/example.js +9 -3
- package/i18n/fetch-languages-sheets.js +5 -4
- package/lib/RemoteCalibrator.min.js +1 -1
- package/lib/RemoteCalibrator.min.js.LICENSE.txt +1 -1
- package/lib/RemoteCalibrator.min.js.map +1 -1
- package/package.json +15 -15
- package/src/WebGazer4RC/.gitattributes +10 -0
- package/src/WebGazer4RC/LICENSE.md +15 -0
- package/src/WebGazer4RC/README.md +142 -0
- package/src/WebGazer4RC/gnu-lgpl-v3.0.md +163 -0
- package/src/WebGazer4RC/gplv3.md +636 -0
- package/src/WebGazer4RC/package-lock.json +1133 -0
- package/src/WebGazer4RC/package.json +28 -0
- package/src/WebGazer4RC/src/dom_util.mjs +27 -0
- package/src/WebGazer4RC/src/facemesh.mjs +150 -0
- package/src/WebGazer4RC/src/index.mjs +1235 -0
- package/src/WebGazer4RC/src/mat.mjs +301 -0
- package/src/WebGazer4RC/src/params.mjs +29 -0
- package/src/WebGazer4RC/src/pupil.mjs +109 -0
- package/src/WebGazer4RC/src/ridgeReg.mjs +104 -0
- package/src/WebGazer4RC/src/ridgeRegThreaded.mjs +161 -0
- package/src/WebGazer4RC/src/ridgeWeightedReg.mjs +125 -0
- package/src/WebGazer4RC/src/ridgeWorker.mjs +135 -0
- package/src/WebGazer4RC/src/util.mjs +348 -0
- package/src/WebGazer4RC/src/util_regression.mjs +240 -0
- package/src/WebGazer4RC/src/worker_scripts/mat.js +306 -0
- package/src/WebGazer4RC/src/worker_scripts/util.js +398 -0
- package/src/WebGazer4RC/test/regression_test.js +182 -0
- package/src/WebGazer4RC/test/run_tests_and_server.sh +24 -0
- package/src/WebGazer4RC/test/util_test.js +60 -0
- package/src/WebGazer4RC/test/webgazerExtract_test.js +40 -0
- package/src/WebGazer4RC/test/webgazer_test.js +160 -0
- package/src/WebGazer4RC/test/www_page_test.js +41 -0
- package/src/const.js +3 -0
- package/src/core.js +8 -0
- package/src/css/distance.scss +40 -0
- package/src/css/panel.scss +32 -1
- package/src/distance/distance.js +4 -4
- package/src/distance/distanceCheck.js +115 -0
- package/src/distance/distanceTrack.js +99 -41
- package/src/{interpupillaryDistance.js → distance/interPupillaryDistance.js} +14 -12
- package/src/gaze/gazeTracker.js +16 -1
- package/src/i18n.js +1 -1
- package/src/index.js +2 -1
- package/src/panel.js +32 -3
- package/webpack.config.js +4 -4
@@ -0,0 +1,301 @@
|
|
1
|
+
const mat = {};
|
2
|
+
/**
|
3
|
+
* Transposes an mxn array
|
4
|
+
* @param {Array.<Array.<Number>>} matrix - of 'M x N' dimensionality
|
5
|
+
* @return {Array.<Array.<Number>>} transposed matrix
|
6
|
+
*/
|
7
|
+
mat.transpose = function(matrix){
|
8
|
+
var m = matrix.length;
|
9
|
+
var n = matrix[0].length;
|
10
|
+
var transposedMatrix = new Array(n);
|
11
|
+
|
12
|
+
for (var i = 0; i < m; i++){
|
13
|
+
for (var j = 0; j < n; j++){
|
14
|
+
if (i === 0) transposedMatrix[j] = new Array(m);
|
15
|
+
transposedMatrix[j][i] = matrix[i][j];
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
return transposedMatrix;
|
20
|
+
};
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Get a sub-matrix of matrix
|
24
|
+
* @param {Array.<Array.<Number>>} matrix - original matrix
|
25
|
+
* @param {Array.<Number>} r - Array of row indices
|
26
|
+
* @param {Number} j0 - Initial column index
|
27
|
+
* @param {Number} j1 - Final column index
|
28
|
+
* @returns {Array} The sub-matrix matrix(r(:),j0:j1)
|
29
|
+
*/
|
30
|
+
mat.getMatrix = function(matrix, r, j0, j1){
|
31
|
+
var X = new Array(r.length),
|
32
|
+
m = j1-j0+1;
|
33
|
+
|
34
|
+
for (var i = 0; i < r.length; i++){
|
35
|
+
X[i] = new Array(m);
|
36
|
+
for (var j = j0; j <= j1; j++){
|
37
|
+
X[i][j-j0] = matrix[r[i]][j];
|
38
|
+
}
|
39
|
+
}
|
40
|
+
return X;
|
41
|
+
};
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Get a submatrix of matrix
|
45
|
+
* @param {Array.<Array.<Number>>} matrix - original matrix
|
46
|
+
* @param {Number} i0 - Initial row index
|
47
|
+
* @param {Number} i1 - Final row index
|
48
|
+
* @param {Number} j0 - Initial column index
|
49
|
+
* @param {Number} j1 - Final column index
|
50
|
+
* @return {Array} The sub-matrix matrix(i0:i1,j0:j1)
|
51
|
+
*/
|
52
|
+
mat.getSubMatrix = function(matrix, i0, i1, j0, j1){
|
53
|
+
var size = j1 - j0 + 1,
|
54
|
+
X = new Array(i1-i0+1);
|
55
|
+
|
56
|
+
for (var i = i0; i <= i1; i++){
|
57
|
+
var subI = i-i0;
|
58
|
+
|
59
|
+
X[subI] = new Array(size);
|
60
|
+
|
61
|
+
for (var j = j0; j <= j1; j++){
|
62
|
+
X[subI][j-j0] = matrix[i][j];
|
63
|
+
}
|
64
|
+
}
|
65
|
+
return X;
|
66
|
+
};
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Linear algebraic matrix multiplication, matrix1 * matrix2
|
70
|
+
* @param {Array.<Array.<Number>>} matrix1
|
71
|
+
* @param {Array.<Array.<Number>>} matrix2
|
72
|
+
* @return {Array.<Array.<Number>>} Matrix product, matrix1 * matrix2
|
73
|
+
*/
|
74
|
+
mat.mult = function(matrix1, matrix2){
|
75
|
+
|
76
|
+
if (matrix2.length != matrix1[0].length){
|
77
|
+
console.log('Matrix inner dimensions must agree:');
|
78
|
+
|
79
|
+
}
|
80
|
+
|
81
|
+
var X = new Array(matrix1.length),
|
82
|
+
Bcolj = new Array(matrix1[0].length);
|
83
|
+
|
84
|
+
for (var j = 0; j < matrix2[0].length; j++){
|
85
|
+
for (var k = 0; k < matrix1[0].length; k++){
|
86
|
+
Bcolj[k] = matrix2[k][j];
|
87
|
+
}
|
88
|
+
for (var i = 0; i < matrix1.length; i++){
|
89
|
+
|
90
|
+
if (j === 0)
|
91
|
+
X[i] = new Array(matrix2[0].length);
|
92
|
+
|
93
|
+
var Arowi = matrix1[i];
|
94
|
+
var s = 0;
|
95
|
+
for (var k = 0; k < matrix1[0].length; k++){
|
96
|
+
s += Arowi[k]*Bcolj[k];
|
97
|
+
}
|
98
|
+
X[i][j] = s;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
return X;
|
102
|
+
};
|
103
|
+
|
104
|
+
|
105
|
+
/**
|
106
|
+
* LUDecomposition to solve A*X = B, based on WEKA code
|
107
|
+
* @param {Array.<Array.<Number>>} A - left matrix of equation to be solved
|
108
|
+
* @param {Array.<Array.<Number>>} B - right matrix of equation to be solved
|
109
|
+
* @return {Array.<Array.<Number>>} X so that L*U*X = B(piv,:)
|
110
|
+
*/
|
111
|
+
mat.LUDecomposition = function(A,B){
|
112
|
+
var LU = new Array(A.length);
|
113
|
+
|
114
|
+
for (var i = 0; i < A.length; i++){
|
115
|
+
LU[i] = new Array(A[0].length);
|
116
|
+
for (var j = 0; j < A[0].length; j++){
|
117
|
+
LU[i][j] = A[i][j];
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
var m = A.length;
|
122
|
+
var n = A[0].length;
|
123
|
+
var piv = new Array(m);
|
124
|
+
for (var i = 0; i < m; i++){
|
125
|
+
piv[i] = i;
|
126
|
+
}
|
127
|
+
var pivsign = 1;
|
128
|
+
var LUrowi = new Array();
|
129
|
+
var LUcolj = new Array(m);
|
130
|
+
// Outer loop.
|
131
|
+
for (var j = 0; j < n; j++){
|
132
|
+
// Make a copy of the j-th column to localize references.
|
133
|
+
for (var i = 0; i < m; i++){
|
134
|
+
LUcolj[i] = LU[i][j];
|
135
|
+
}
|
136
|
+
// Apply previous transformations.
|
137
|
+
for (var i = 0; i < m; i++){
|
138
|
+
LUrowi = LU[i];
|
139
|
+
// Most of the time is spent in the following dot product.
|
140
|
+
var kmax = Math.min(i,j);
|
141
|
+
var s = 0;
|
142
|
+
for (var k = 0; k < kmax; k++){
|
143
|
+
s += LUrowi[k]*LUcolj[k];
|
144
|
+
}
|
145
|
+
LUrowi[j] = LUcolj[i] -= s;
|
146
|
+
}
|
147
|
+
// Find pivot and exchange if necessary.
|
148
|
+
var p = j;
|
149
|
+
for (var i = j+1; i < m; i++){
|
150
|
+
if (Math.abs(LUcolj[i]) > Math.abs(LUcolj[p])){
|
151
|
+
p = i;
|
152
|
+
}
|
153
|
+
}
|
154
|
+
if (p != j){
|
155
|
+
for (var k = 0; k < n; k++){
|
156
|
+
var t = LU[p][k];
|
157
|
+
LU[p][k] = LU[j][k];
|
158
|
+
LU[j][k] = t;
|
159
|
+
}
|
160
|
+
var k = piv[p];
|
161
|
+
piv[p] = piv[j];
|
162
|
+
piv[j] = k;
|
163
|
+
pivsign = -pivsign;
|
164
|
+
}
|
165
|
+
// Compute multipliers.
|
166
|
+
if (j < m & LU[j][j] != 0){
|
167
|
+
for (var i = j+1; i < m; i++){
|
168
|
+
LU[i][j] /= LU[j][j];
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|
172
|
+
if (B.length != m){
|
173
|
+
console.log('Matrix row dimensions must agree.');
|
174
|
+
}
|
175
|
+
for (var j = 0; j < n; j++){
|
176
|
+
if (LU[j][j] === 0){
|
177
|
+
console.log('Matrix is singular.')
|
178
|
+
}
|
179
|
+
}
|
180
|
+
var nx = B[0].length;
|
181
|
+
var X = self.webgazer.mat.getMatrix(B,piv,0,nx-1);
|
182
|
+
// Solve L*Y = B(piv,:)
|
183
|
+
for (var k = 0; k < n; k++){
|
184
|
+
for (var i = k+1; i < n; i++){
|
185
|
+
for (var j = 0; j < nx; j++){
|
186
|
+
X[i][j] -= X[k][j]*LU[i][k];
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
// Solve U*X = Y;
|
191
|
+
for (var k = n-1; k >= 0; k--){
|
192
|
+
for (var j = 0; j < nx; j++){
|
193
|
+
X[k][j] /= LU[k][k];
|
194
|
+
}
|
195
|
+
for (var i = 0; i < k; i++){
|
196
|
+
for (var j = 0; j < nx; j++){
|
197
|
+
X[i][j] -= X[k][j]*LU[i][k];
|
198
|
+
}
|
199
|
+
}
|
200
|
+
}
|
201
|
+
return X;
|
202
|
+
};
|
203
|
+
|
204
|
+
/**
|
205
|
+
* Least squares solution of A*X = B, based on WEKA code
|
206
|
+
* @param {Array.<Array.<Number>>} A - left side matrix to be solved
|
207
|
+
* @param {Array.<Array.<Number>>} B - a matrix with as many rows as A and any number of columns.
|
208
|
+
* @return {Array.<Array.<Number>>} X - that minimizes the two norms of QR*X-B.
|
209
|
+
*/
|
210
|
+
mat.QRDecomposition = function(A, B){
|
211
|
+
// Initialize.
|
212
|
+
var QR = new Array(A.length);
|
213
|
+
|
214
|
+
for (var i = 0; i < A.length; i++){
|
215
|
+
QR[i] = new Array(A[0].length);
|
216
|
+
for (var j = 0; j < A[0].length; j++){
|
217
|
+
QR[i][j] = A[i][j];
|
218
|
+
}
|
219
|
+
}
|
220
|
+
var m = A.length;
|
221
|
+
var n = A[0].length;
|
222
|
+
var Rdiag = new Array(n);
|
223
|
+
var nrm;
|
224
|
+
|
225
|
+
// Main loop.
|
226
|
+
for (var k = 0; k < n; k++){
|
227
|
+
// Compute 2-norm of k-th column without under/overflow.
|
228
|
+
nrm = 0;
|
229
|
+
for (var i = k; i < m; i++){
|
230
|
+
nrm = Math.hypot(nrm,QR[i][k]);
|
231
|
+
}
|
232
|
+
if (nrm != 0){
|
233
|
+
// Form k-th Householder vector.
|
234
|
+
if (QR[k][k] < 0){
|
235
|
+
nrm = -nrm;
|
236
|
+
}
|
237
|
+
for (var i = k; i < m; i++){
|
238
|
+
QR[i][k] /= nrm;
|
239
|
+
}
|
240
|
+
QR[k][k] += 1;
|
241
|
+
|
242
|
+
// Apply transformation to remaining columns.
|
243
|
+
for (var j = k+1; j < n; j++){
|
244
|
+
var s = 0;
|
245
|
+
for (var i = k; i < m; i++){
|
246
|
+
s += QR[i][k]*QR[i][j];
|
247
|
+
}
|
248
|
+
s = -s/QR[k][k];
|
249
|
+
for (var i = k; i < m; i++){
|
250
|
+
QR[i][j] += s*QR[i][k];
|
251
|
+
}
|
252
|
+
}
|
253
|
+
}
|
254
|
+
Rdiag[k] = -nrm;
|
255
|
+
}
|
256
|
+
if (B.length != m){
|
257
|
+
console.log('Matrix row dimensions must agree.');
|
258
|
+
}
|
259
|
+
for (var j = 0; j < n; j++){
|
260
|
+
if (Rdiag[j] === 0)
|
261
|
+
console.log('Matrix is rank deficient');
|
262
|
+
}
|
263
|
+
// Copy right hand side
|
264
|
+
var nx = B[0].length;
|
265
|
+
var X = new Array(B.length);
|
266
|
+
for(var i=0; i<B.length; i++){
|
267
|
+
X[i] = new Array(B[0].length);
|
268
|
+
}
|
269
|
+
for (var i = 0; i < B.length; i++){
|
270
|
+
for (var j = 0; j < B[0].length; j++){
|
271
|
+
X[i][j] = B[i][j];
|
272
|
+
}
|
273
|
+
}
|
274
|
+
// Compute Y = transpose(Q)*B
|
275
|
+
for (var k = 0; k < n; k++){
|
276
|
+
for (var j = 0; j < nx; j++){
|
277
|
+
var s = 0.0;
|
278
|
+
for (var i = k; i < m; i++){
|
279
|
+
s += QR[i][k]*X[i][j];
|
280
|
+
}
|
281
|
+
s = -s/QR[k][k];
|
282
|
+
for (var i = k; i < m; i++){
|
283
|
+
X[i][j] += s*QR[i][k];
|
284
|
+
}
|
285
|
+
}
|
286
|
+
}
|
287
|
+
// Solve R*X = Y;
|
288
|
+
for (var k = n-1; k >= 0; k--){
|
289
|
+
for (var j = 0; j < nx; j++){
|
290
|
+
X[k][j] /= Rdiag[k];
|
291
|
+
}
|
292
|
+
for (var i = 0; i < k; i++){
|
293
|
+
for (var j = 0; j < nx; j++){
|
294
|
+
X[i][j] -= X[k][j]*QR[i][k];
|
295
|
+
}
|
296
|
+
}
|
297
|
+
}
|
298
|
+
return mat.getSubMatrix(X,0,n-1,0,nx-1);
|
299
|
+
}
|
300
|
+
|
301
|
+
export default mat;
|
@@ -0,0 +1,29 @@
|
|
1
|
+
const params = {
|
2
|
+
moveTickSize: 50,
|
3
|
+
videoContainerId: 'webgazerVideoContainer',
|
4
|
+
videoElementId: 'webgazerVideoFeed',
|
5
|
+
videoElementCanvasId: 'webgazerVideoCanvas',
|
6
|
+
faceOverlayId: 'webgazerFaceOverlay',
|
7
|
+
faceFeedbackBoxId: 'webgazerFaceFeedbackBox',
|
8
|
+
gazeDotId: 'webgazerGazeDot',
|
9
|
+
videoViewerWidth: 320,
|
10
|
+
videoViewerHeight: 240,
|
11
|
+
faceFeedbackBoxRatio: 0.66,
|
12
|
+
// View options
|
13
|
+
showVideo: true,
|
14
|
+
mirrorVideo: true,
|
15
|
+
showFaceOverlay: true,
|
16
|
+
showFaceFeedbackBox: true,
|
17
|
+
showGazeDot: true,
|
18
|
+
camConstraints: { video: { width: { min: 320, ideal: 640, max: 1920 }, height: { min: 240, ideal: 480, max: 1080 }, facingMode: "user" } },
|
19
|
+
dataTimestep: 50,
|
20
|
+
showVideoPreview: true,
|
21
|
+
applyKalmanFilter: true,
|
22
|
+
saveDataAcrossSessions: true,
|
23
|
+
// Whether or not to store accuracy eigenValues, used by the calibration example file
|
24
|
+
storingPoints: false,
|
25
|
+
////
|
26
|
+
videoIsOn: false,
|
27
|
+
};
|
28
|
+
|
29
|
+
export default params;
|
@@ -0,0 +1,109 @@
|
|
1
|
+
const pupil = {};
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Returns intensity value at x,y position of a pixels image
|
5
|
+
* @param {Array} pixels - array of size width*height
|
6
|
+
* @param {Number} x - input x value
|
7
|
+
* @param {Number} y - input y value
|
8
|
+
* @param {Number} width - width of pixels image
|
9
|
+
* @returns {Number} - intensity value in [0,255]
|
10
|
+
*/
|
11
|
+
const getValue = function (pixels, x, y, width){
|
12
|
+
return pixels[y * width + x];
|
13
|
+
};
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Computes summation area table/integral image of a pixel matrix
|
17
|
+
* @param {Array} pixels value of eye area
|
18
|
+
* @param {Number} width - of image in 'pixels'
|
19
|
+
* @param {Number} height - of image in 'pixels'
|
20
|
+
* @returns {Array} - integral image
|
21
|
+
*/
|
22
|
+
const getSumTable = function (pixels, width, height){
|
23
|
+
var integralImage = new Array(width);
|
24
|
+
var sumx = 0;
|
25
|
+
var sumy = 0;
|
26
|
+
|
27
|
+
for (var i = 0; i < width; i++){
|
28
|
+
integralImage[i] = new Array(height);
|
29
|
+
sumx += getValue(pixels, i, 0, width);
|
30
|
+
integralImage[i][0] = sumx;
|
31
|
+
}
|
32
|
+
|
33
|
+
for (var i = 0; i < height; i++){
|
34
|
+
sumy += getValue(pixels, 0, i, width);
|
35
|
+
integralImage[0][i] = sumy;
|
36
|
+
}
|
37
|
+
|
38
|
+
for (var x = 1; x < width; x++){
|
39
|
+
for (var y = 1; y < height; y++){
|
40
|
+
integralImage[x][y] = getValue(pixels, x, y, width) + integralImage[x - 1][y] + integralImage[x][y - 1] - integralImage[x - 1][y - 1];
|
41
|
+
}
|
42
|
+
}
|
43
|
+
return integralImage;
|
44
|
+
};
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Detects a pupil in a set of pixels
|
48
|
+
* @param {Array} pixels - patch of pixels to look for pupil into
|
49
|
+
* @param {Number} width - of pixel patch
|
50
|
+
* @param {Number} height - of pixel patch
|
51
|
+
* @return {Array} coordinate of the bottom right corner and width of the best fitted pupil
|
52
|
+
*/
|
53
|
+
const getSinglePupil = function (pixels, width, height){
|
54
|
+
var summedAreaTable = getSumTable(pixels, width, height);
|
55
|
+
var bestAvgScore = 999999; //want to minimize this score
|
56
|
+
var bestPoint = [0, 0]; //bottom right corner of best fitted pupil
|
57
|
+
var bestHalfWidth = 0; //corresponding half width of the best fitted pupil
|
58
|
+
var offset = Math.floor(width / 10.0); //padding
|
59
|
+
//halfWidth could also start at 1, but this makes it faster
|
60
|
+
for (var halfWidth = Math.floor(height / 10.0); halfWidth < width / 2; halfWidth++){
|
61
|
+
//think of a sliding rectangular window of width halfWidth*2 that goes through the whole eye pixel matrix and does the following:
|
62
|
+
//1) computes the irisArea, which is the total intensity of the iris
|
63
|
+
//2) computes the scleraIrisArea, which is multiple rows of pixels including the sclera and iris.
|
64
|
+
//3) computes avg, which is the intensity of the area divided by the number of pixels.
|
65
|
+
//start at the bottom right of the rectangle!not top left
|
66
|
+
for (var x = halfWidth; x < width - offset; x++){
|
67
|
+
for (var y = halfWidth; y < height - offset; y++){
|
68
|
+
//evaluate area by the formula found on wikipedia about the summed area table: I(D)+I(A)-I(B)-I(C)
|
69
|
+
var irisArea = summedAreaTable[x + offset][y + offset] + summedAreaTable[x + offset - halfWidth][y + offset - halfWidth] - summedAreaTable[x + offset][y + offset - halfWidth] - summedAreaTable[x + offset - halfWidth][y + offset];
|
70
|
+
var avgScore = 1.0 * irisArea / ((halfWidth + 1) * (halfWidth + 1)) + 1;
|
71
|
+
//summation area table again
|
72
|
+
var scleraIrisArea = ((1.0 * summedAreaTable[width - 1 - offset][y + offset] + summedAreaTable[0 + offset][y + offset - halfWidth] - summedAreaTable[0 + offset][y + offset] - summedAreaTable[width - 1 - offset][y + offset - halfWidth]) - irisArea);
|
73
|
+
//minimize avgScore/scleraIrisArea. 150 is too high, might have to change since it's closer to white
|
74
|
+
if ((avgScore) / scleraIrisArea < bestAvgScore && avgScore < 150){
|
75
|
+
bestAvgScore = (avgScore) / scleraIrisArea;
|
76
|
+
bestPoint = [x + offset, y + offset];
|
77
|
+
bestHalfWidth = halfWidth;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
return [bestPoint, bestHalfWidth];
|
83
|
+
};
|
84
|
+
|
85
|
+
/**
|
86
|
+
* Given an object with two eye patches it finds the location of the detected pupils
|
87
|
+
* @param {Object} eyesObj - left and right detected eye patches
|
88
|
+
* @return {Object} eyesObj - updated eye patches with information about pupils' locations
|
89
|
+
*/
|
90
|
+
const getPupils = function(eyesObj) {
|
91
|
+
if (!eyesObj) {
|
92
|
+
return eyesObj;
|
93
|
+
}
|
94
|
+
if (!eyesObj.left.blink) {
|
95
|
+
eyesObj.left.pupil = getSinglePupil(Array.prototype.slice.call(webgazer.util.grayscale(eyesObj.left.patch.data, eyesObj.left.width, eyesObj.left.height)), eyesObj.left.width, eyesObj.left.height);
|
96
|
+
eyesObj.left.pupil[0][0] -= eyesObj.left.pupil[1];
|
97
|
+
eyesObj.left.pupil[0][1] -= eyesObj.left.pupil[1];
|
98
|
+
}
|
99
|
+
if (!eyesObj.right.blink) {
|
100
|
+
eyesObj.right.pupil = getSinglePupil(Array.prototype.slice.call(webgazer.util.grayscale(eyesObj.right.patch.data, eyesObj.right.width, eyesObj.right.height)), eyesObj.right.width, eyesObj.right.height);
|
101
|
+
eyesObj.right.pupil[0][0] -= eyesObj.right.pupil[1];
|
102
|
+
eyesObj.right.pupil[0][1] -= eyesObj.right.pupil[1];
|
103
|
+
}
|
104
|
+
return eyesObj;
|
105
|
+
}
|
106
|
+
|
107
|
+
pupil.getPupils = getPupils;
|
108
|
+
|
109
|
+
export default pupil;
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import util from './util.mjs';
|
2
|
+
import util_regression from './util_regression.mjs';
|
3
|
+
import params from './params.mjs';
|
4
|
+
|
5
|
+
const reg = {};
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Constructor of RidgeReg object,
|
9
|
+
* this object allow to perform ridge regression
|
10
|
+
* @constructor
|
11
|
+
*/
|
12
|
+
reg.RidgeReg = function() {
|
13
|
+
this.init();
|
14
|
+
};
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Initialize new arrays and initialize Kalman filter.
|
18
|
+
*/
|
19
|
+
reg.RidgeReg.prototype.init = util_regression.InitRegression
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Add given data from eyes
|
23
|
+
* @param {Object} eyes - eyes where extract data to add
|
24
|
+
* @param {Object} screenPos - The current screen point
|
25
|
+
* @param {Object} type - The type of performed action
|
26
|
+
*/
|
27
|
+
reg.RidgeReg.prototype.addData = util_regression.addData
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Try to predict coordinates from pupil data
|
31
|
+
* after apply linear regression on data set
|
32
|
+
* @param {Object} eyesObj - The current user eyes object
|
33
|
+
* @returns {Object}
|
34
|
+
*/
|
35
|
+
reg.RidgeReg.prototype.predict = function(eyesObj) {
|
36
|
+
if (!eyesObj || this.eyeFeaturesClicks.length === 0) {
|
37
|
+
return null;
|
38
|
+
}
|
39
|
+
var acceptTime = performance.now() - this.trailTime;
|
40
|
+
var trailX = [];
|
41
|
+
var trailY = [];
|
42
|
+
var trailFeat = [];
|
43
|
+
for (var i = 0; i < this.trailDataWindow; i++) {
|
44
|
+
if (this.trailTimes.get(i) > acceptTime) {
|
45
|
+
trailX.push(this.screenXTrailArray.get(i));
|
46
|
+
trailY.push(this.screenYTrailArray.get(i));
|
47
|
+
trailFeat.push(this.eyeFeaturesTrail.get(i));
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
var screenXArray = this.screenXClicksArray.data.concat(trailX);
|
52
|
+
var screenYArray = this.screenYClicksArray.data.concat(trailY);
|
53
|
+
var eyeFeatures = this.eyeFeaturesClicks.data.concat(trailFeat);
|
54
|
+
|
55
|
+
var coefficientsX = util_regression.ridge(screenXArray, eyeFeatures, this.ridgeParameter);
|
56
|
+
var coefficientsY = util_regression.ridge(screenYArray, eyeFeatures, this.ridgeParameter);
|
57
|
+
|
58
|
+
var eyeFeats = util.getEyeFeats(eyesObj);
|
59
|
+
var predictedX = 0;
|
60
|
+
for(var i=0; i< eyeFeats.length; i++){
|
61
|
+
predictedX += eyeFeats[i] * coefficientsX[i];
|
62
|
+
}
|
63
|
+
var predictedY = 0;
|
64
|
+
for(var i=0; i< eyeFeats.length; i++){
|
65
|
+
predictedY += eyeFeats[i] * coefficientsY[i];
|
66
|
+
}
|
67
|
+
|
68
|
+
predictedX = Math.floor(predictedX);
|
69
|
+
predictedY = Math.floor(predictedY);
|
70
|
+
|
71
|
+
if (params.applyKalmanFilter) {
|
72
|
+
// Update Kalman model, and get prediction
|
73
|
+
var newGaze = [predictedX, predictedY]; // [20200607 xk] Should we use a 1x4 vector?
|
74
|
+
newGaze = this.kalman.update(newGaze);
|
75
|
+
|
76
|
+
return {
|
77
|
+
x: newGaze[0],
|
78
|
+
y: newGaze[1]
|
79
|
+
};
|
80
|
+
} else {
|
81
|
+
return {
|
82
|
+
x: predictedX,
|
83
|
+
y: predictedY
|
84
|
+
};
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
88
|
+
reg.RidgeReg.prototype.setData = util_regression.setData;
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Return the data
|
92
|
+
* @returns {Array.<Object>|*}
|
93
|
+
*/
|
94
|
+
reg.RidgeReg.prototype.getData = function() {
|
95
|
+
return this.dataClicks.data;
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* The RidgeReg object name
|
100
|
+
* @type {string}
|
101
|
+
*/
|
102
|
+
reg.RidgeReg.prototype.name = 'ridge';
|
103
|
+
|
104
|
+
export default reg;
|
@@ -0,0 +1,161 @@
|
|
1
|
+
import util from './util.mjs';
|
2
|
+
import numeric from 'numeric';
|
3
|
+
import util_regression from './util_regression.mjs';
|
4
|
+
import params from './params.mjs';
|
5
|
+
|
6
|
+
const reg = {};
|
7
|
+
|
8
|
+
var ridgeParameter = Math.pow(10,-5);
|
9
|
+
var dataWindow = 700;
|
10
|
+
var weights = {'X':[0],'Y':[0]};
|
11
|
+
var trailDataWindow = 10;
|
12
|
+
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Constructor of RidgeRegThreaded object,
|
16
|
+
* it retrieve data window, and prepare a worker,
|
17
|
+
* this object allow to perform threaded ridge regression
|
18
|
+
* @constructor
|
19
|
+
*/
|
20
|
+
reg.RidgeRegThreaded = function() {
|
21
|
+
this.init();
|
22
|
+
};
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Initialize new arrays and initialize Kalman filter.
|
26
|
+
*/
|
27
|
+
reg.RidgeRegThreaded.prototype.init = function() {
|
28
|
+
this.screenXClicksArray = new util.DataWindow(dataWindow);
|
29
|
+
this.screenYClicksArray = new util.DataWindow(dataWindow);
|
30
|
+
this.eyeFeaturesClicks = new util.DataWindow(dataWindow);
|
31
|
+
|
32
|
+
this.screenXTrailArray = new util.DataWindow(trailDataWindow);
|
33
|
+
this.screenYTrailArray = new util.DataWindow(trailDataWindow);
|
34
|
+
this.eyeFeaturesTrail = new util.DataWindow(trailDataWindow);
|
35
|
+
|
36
|
+
this.dataClicks = new util.DataWindow(dataWindow);
|
37
|
+
this.dataTrail = new util.DataWindow(dataWindow);
|
38
|
+
|
39
|
+
// Place the src/ridgeworker.js file into the same directory as your html file.
|
40
|
+
if (!this.worker) {
|
41
|
+
this.worker = new Worker('ridgeWorker.mjs'); // [20200708] TODO: Figure out how to make this inline
|
42
|
+
this.worker.onerror = function(err) { console.log(err.message); };
|
43
|
+
this.worker.onmessage = function(evt) {
|
44
|
+
weights.X = evt.data.X;
|
45
|
+
weights.Y = evt.data.Y;
|
46
|
+
};
|
47
|
+
console.log('initialized worker');
|
48
|
+
}
|
49
|
+
|
50
|
+
// Initialize Kalman filter [20200608 xk] what do we do about parameters?
|
51
|
+
// [20200611 xk] unsure what to do w.r.t. dimensionality of these matrices. So far at least
|
52
|
+
// by my own anecdotal observation a 4x1 x vector seems to work alright
|
53
|
+
var F = [ [1, 0, 1, 0],
|
54
|
+
[0, 1, 0, 1],
|
55
|
+
[0, 0, 1, 0],
|
56
|
+
[0, 0, 0, 1]];
|
57
|
+
|
58
|
+
//Parameters Q and R may require some fine tuning
|
59
|
+
var Q = [ [1/4, 0, 1/2, 0],
|
60
|
+
[0, 1/4, 0, 1/2],
|
61
|
+
[1/2, 0, 1, 0],
|
62
|
+
[0, 1/2, 0, 1]];// * delta_t
|
63
|
+
var delta_t = 1/10; // The amount of time between frames
|
64
|
+
Q = numeric.mul(Q, delta_t);
|
65
|
+
|
66
|
+
var H = [ [1, 0, 0, 0, 0, 0],
|
67
|
+
[0, 1, 0, 0, 0, 0],
|
68
|
+
[0, 0, 1, 0, 0, 0],
|
69
|
+
[0, 0, 0, 1, 0, 0]];
|
70
|
+
var H = [ [1, 0, 0, 0],
|
71
|
+
[0, 1, 0, 0]];
|
72
|
+
var pixel_error = 47; //We will need to fine tune this value [20200611 xk] I just put a random value here
|
73
|
+
|
74
|
+
//This matrix represents the expected measurement error
|
75
|
+
var R = numeric.mul(numeric.identity(2), pixel_error);
|
76
|
+
|
77
|
+
var P_initial = numeric.mul(numeric.identity(4), 0.0001); //Initial covariance matrix
|
78
|
+
var x_initial = [[500], [500], [0], [0]]; // Initial measurement matrix
|
79
|
+
|
80
|
+
this.kalman = new util_regression.KalmanFilter(F, H, Q, R, P_initial, x_initial);
|
81
|
+
}
|
82
|
+
/**
|
83
|
+
* Add given data from eyes
|
84
|
+
* @param {Object} eyes - eyes where extract data to add
|
85
|
+
* @param {Object} screenPos - The current screen point
|
86
|
+
* @param {Object} type - The type of performed action
|
87
|
+
*/
|
88
|
+
reg.RidgeRegThreaded.prototype.addData = function(eyes, screenPos, type) {
|
89
|
+
if (!eyes) {
|
90
|
+
return;
|
91
|
+
}
|
92
|
+
//not doing anything with blink at present
|
93
|
+
// if (eyes.left.blink || eyes.right.blink) {
|
94
|
+
// return;
|
95
|
+
// }
|
96
|
+
this.worker.postMessage({'eyes':util.getEyeFeats(eyes), 'screenPos':screenPos, 'type':type});
|
97
|
+
};
|
98
|
+
|
99
|
+
/**
|
100
|
+
* Try to predict coordinates from pupil data
|
101
|
+
* after apply linear regression on data set
|
102
|
+
* @param {Object} eyesObj - The current user eyes object
|
103
|
+
* @returns {Object}
|
104
|
+
*/
|
105
|
+
reg.RidgeRegThreaded.prototype.predict = function(eyesObj) {
|
106
|
+
// console.log('LOGGING..');
|
107
|
+
if (!eyesObj) {
|
108
|
+
return null;
|
109
|
+
}
|
110
|
+
var coefficientsX = weights.X;
|
111
|
+
var coefficientsY = weights.Y;
|
112
|
+
|
113
|
+
var eyeFeats = util.getEyeFeats(eyesObj);
|
114
|
+
var predictedX = 0, predictedY = 0;
|
115
|
+
for(var i=0; i< eyeFeats.length; i++){
|
116
|
+
predictedX += eyeFeats[i] * coefficientsX[i];
|
117
|
+
predictedY += eyeFeats[i] * coefficientsY[i];
|
118
|
+
}
|
119
|
+
|
120
|
+
predictedX = Math.floor(predictedX);
|
121
|
+
predictedY = Math.floor(predictedY);
|
122
|
+
|
123
|
+
if (params.applyKalmanFilter) {
|
124
|
+
// Update Kalman model, and get prediction
|
125
|
+
var newGaze = [predictedX, predictedY]; // [20200607 xk] Should we use a 1x4 vector?
|
126
|
+
newGaze = this.kalman.update(newGaze);
|
127
|
+
|
128
|
+
return {
|
129
|
+
x: newGaze[0],
|
130
|
+
y: newGaze[1]
|
131
|
+
};
|
132
|
+
} else {
|
133
|
+
return {
|
134
|
+
x: predictedX,
|
135
|
+
y: predictedY
|
136
|
+
};
|
137
|
+
}
|
138
|
+
};
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Add given data to current data set then,
|
142
|
+
* replace current data member with given data
|
143
|
+
* @param {Array.<Object>} data - The data to set
|
144
|
+
*/
|
145
|
+
reg.RidgeRegThreaded.prototype.setData = util_regression.setData
|
146
|
+
|
147
|
+
/**
|
148
|
+
* Return the data
|
149
|
+
* @returns {Array.<Object>|*}
|
150
|
+
*/
|
151
|
+
reg.RidgeRegThreaded.prototype.getData = function() {
|
152
|
+
return this.dataClicks.data;
|
153
|
+
};
|
154
|
+
|
155
|
+
/**
|
156
|
+
* The RidgeRegThreaded object name
|
157
|
+
* @type {string}
|
158
|
+
*/
|
159
|
+
reg.RidgeRegThreaded.prototype.name = 'ridge';
|
160
|
+
|
161
|
+
export default reg;
|