remote-calibrator 0.3.0 → 0.5.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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;
|