-
Notifications
You must be signed in to change notification settings - Fork 15
/
detect.js
260 lines (201 loc) · 8 KB
/
detect.js
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// Canvas setup
const drawCtx = drawCanvas.getContext('2d');
// Global flags
const flipHorizontal = true;
let stopPrediction = false;
let isPlaying = false,
gotMetadata = false;
let firstRun = true;
// check if metadata is ready - we need the sourceVideo size
sourceVideo.onloadedmetadata = () => {
console.log("video metadata ready");
gotMetadata = true;
if (isPlaying)
load()
};
// Check if the sourceVideo has started playing
sourceVideo.onplaying = () => {
console.log("video playing");
isPlaying = true;
if (gotMetadata) {
load()
}
};
function load(multiplier=0.75, stride=16) {
sourceVideo.width = sourceVideo.videoWidth;
sourceVideo.height = sourceVideo.videoHeight;
// Canvas results for displaying masks
drawCanvas.width = sourceVideo.videoWidth;
drawCanvas.height = sourceVideo.videoHeight;
userMessage.innerText = "Waiting for Machine Learning model to load...";
console.log(`loading BodyPix with multiplier ${multiplier} and stride ${stride}`);
bodyPix.load({multiplier: multiplier, stride: stride, quantBytes: 4})
.then(net => predictLoop(net))
.catch(err => console.error(err));
}
async function predictLoop(net) {
stopPrediction = false;
let lastFaceArray = new Int32Array(sourceVideo.width * sourceVideo.height);
let alerts = 0;
let alertTimeout = false;
enableDashboard(firstRun); // Show the dashboard
// Timer to update the face mask
let updateFace = true;
setInterval(() => {
updateFace = !updateFace;
}, 1000);
while (isPlaying && !stopPrediction) {
// BodyPix setup
const segmentPersonConfig = {
flipHorizontal: flipHorizontal, // Flip for webcam
maxDetections: 1, // only look at one person in this model
scoreThreshold: 0.5,
segmentationThreshold: 0.6, // default is 0.7
};
const segmentation = await net.segmentPersonParts(sourceVideo, segmentPersonConfig);
const faceThreshold = 0.9;
const touchThreshold = 0.01;
const numPixels = segmentation.width * segmentation.height;
// skip if noting is there
if (segmentation.allPoses[0] === undefined) {
// console.info("No segmentation data");
continue;
}
// Draw the data to canvas
draw(segmentation);
// Verify there is a good quality face before doing anything
// I am assuming a consistent array order
let nose = segmentation.allPoses[0].keypoints[0].score > faceThreshold;
let leftEye = segmentation.allPoses[0].keypoints[1].score > faceThreshold;
let rightEye = segmentation.allPoses[0].keypoints[2].score > faceThreshold;
// Check for hands if there is a nose or eyes
if (nose && (leftEye || rightEye)) {
// Look for overlaps where the hand is and the face used to be
// Create an array of just face values
let faceArray = segmentation.data.map(val => {
if (val === 0 || val === 1) return val;
else return -1;
});
// Get the hand array
let handArray = segmentation.data.map(val => {
if (val === 10 || val === 11) return val;
else return -1;
});
let facePixels = 0;
let score = 0;
for (let x = 0; x < lastFaceArray.length; x++) {
// Count the number of face pixels
if (lastFaceArray[x] > -1)
facePixels++;
// If the hand is overlapping where the face used to be
if (lastFaceArray[x] > -1 && handArray[x] > -1)
score++;
}
let multiFaceArray = arrayToMatrix(faceArray, segmentation.width);
let multiHandArray = arrayToMatrix(handArray, segmentation.width);
let touchScore = touchingCheck(multiFaceArray, multiHandArray, 10);
score += touchScore;
// Update the old face according to the timer
if (updateFace)
lastFaceArray = faceArray;
updateStats(alertTimeout);
// Handle alerts
if (score > facePixels * touchThreshold && !alertTimeout) {
console.info(` numPixels: ${numPixels} \n facePixels: ${facePixels}\n score: ${score}, touchScore: ${touchScore}\n` +
` facePixels%: ${facePixels / numPixels}\n touch%: ${score / facePixels}`);
alerts++;
console.log("alert!!!", alerts);
// User alerts
if (beepToggle.checked)
beep(350, 150, 0);
if (notifyToggle.checked)
notify(`You touched your face! That's ${touches+1} times now`);
alertTimeout = true;
setTimeout(() => {
console.log("resuming alerts");
alertTimeout = false;
}, alertTimeoutEntry.value * 1000)
}
}
}
}
// Checks if there is a face pixel above, below, left or right to this pixel
function touchingCheck(matrix1, matrix2, padding) {
let count = 0;
for (let y = padding; y < matrix1.length - padding; y++)
for (let x = padding; x < matrix1[0].length - padding; x++) {
if (matrix1[y][x] > -1) {
for (let p = 0; p < padding; p++) {
// if the hand is left or right, above or below the current face segment
if (matrix2[y][x - p] > -1 || matrix2[y][x + p] > -1 ||
matrix2[y - p][x] > -1 || matrix2[y + p][x] > -1) {
count++;
}
}
}
}
return count
}
// Use the bodyPix draw API's
function draw(personSegmentation) {
if (showMaskToggle.checked) {
let targetSegmentation = personSegmentation;
// Draw a mask of the body segments - useful for debugging
// Just show the face and hand parts
targetSegmentation.data = personSegmentation.data.map(val => {
if (val !== 0 && val !== 1 && val !== 10 && val !== 11)
return -1;
else
return val;
});
const coloredPartImage = bodyPix.toColoredPartMask(targetSegmentation);
const opacity = 0.7;
const maskBlurAmount = 0;
bodyPix.drawMask(
drawCanvas, sourceVideo, coloredPartImage, opacity, maskBlurAmount,
flipHorizontal);
}
// drawMask clears the canvas, drawKeypoints doesn't
if (showMaskToggle.checked === false) {
// bodyPix.drawMask redraws the canvas. Clear with not
drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);
}
// Show dots from pose detection
if (showPointsToggle.checked) {
personSegmentation.allPoses.forEach(pose => {
if (flipHorizontal) {
pose = bodyPix.flipPoseHorizontal(pose, personSegmentation.width);
}
drawKeypoints(pose.keypoints, 0.9, drawCtx);
});
}
}
// Draw dots
function drawKeypoints(keypoints, minConfidence, ctx, color = 'aqua') {
for (let i = 0; i < keypoints.length; i++) {
const keypoint = keypoints[i];
if (keypoint.score < minConfidence) {
continue;
}
const {y, x} = keypoint.position;
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
}
}
// Helper function to convert an arrow into a matrix for easier pixel proximity functions
function arrayToMatrix(arr, rowLength) {
let newArray = [];
// Check
if (arr.length % rowLength > 0 || rowLength < 1) {
console.log("array not divisible by rowLength ", arr, rowLength);
return
}
let rows = arr.length / rowLength;
for (let x = 0; x < rows; x++) {
let b = arr.slice(x * rowLength, x * rowLength + rowLength);
newArray.push(b);
}
return newArray;
}