-
-
Notifications
You must be signed in to change notification settings - Fork 149
/
webcam.js
207 lines (190 loc) · 8 KB
/
webcam.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
/**
* FaceAPI Demo for Browsers
* Loaded via `webcam.html`
*/
import * as faceapi from '../dist/face-api.esm.js'; // use when in dev mode
// import * as faceapi from '@vladmandic/face-api'; // use when downloading face-api as npm
// configuration options
const modelPath = '../model/'; // path to model folder that will be loaded using http
// const modelPath = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/'; // path to model folder that will be loaded using http
const minScore = 0.2; // minimum score
const maxResults = 5; // maximum number of results to return
let optionsSSDMobileNet;
// helper function to pretty-print json object to string
function str(json) {
let text = '<font color="lightblue">';
text += json ? JSON.stringify(json).replace(/{|}|"|\[|\]/g, '').replace(/,/g, ', ') : '';
text += '</font>';
return text;
}
// helper function to print strings to html document as a log
function log(...txt) {
console.log(...txt); // eslint-disable-line no-console
const div = document.getElementById('log');
if (div) div.innerHTML += `<br>${txt}`;
}
// helper function to draw detected faces
function drawFaces(canvas, data, fps) {
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw title
ctx.font = 'small-caps 20px "Segoe UI"';
ctx.fillStyle = 'white';
ctx.fillText(`FPS: ${fps}`, 10, 25);
for (const person of data) {
// draw box around each face
ctx.lineWidth = 3;
ctx.strokeStyle = 'deepskyblue';
ctx.fillStyle = 'deepskyblue';
ctx.globalAlpha = 0.6;
ctx.beginPath();
ctx.rect(person.detection.box.x, person.detection.box.y, person.detection.box.width, person.detection.box.height);
ctx.stroke();
ctx.globalAlpha = 1;
// const expression = person.expressions.sort((a, b) => Object.values(a)[0] - Object.values(b)[0]);
const expression = Object.entries(person.expressions).sort((a, b) => b[1] - a[1]);
ctx.fillStyle = 'black';
ctx.fillText(`gender: ${Math.round(100 * person.genderProbability)}% ${person.gender}`, person.detection.box.x, person.detection.box.y - 59);
ctx.fillText(`expression: ${Math.round(100 * expression[0][1])}% ${expression[0][0]}`, person.detection.box.x, person.detection.box.y - 41);
ctx.fillText(`age: ${Math.round(person.age)} years`, person.detection.box.x, person.detection.box.y - 23);
ctx.fillText(`roll:${person.angle.roll.toFixed(3)} pitch:${person.angle.pitch.toFixed(3)} yaw:${person.angle.yaw.toFixed(3)}`, person.detection.box.x, person.detection.box.y - 5);
ctx.fillStyle = 'lightblue';
ctx.fillText(`gender: ${Math.round(100 * person.genderProbability)}% ${person.gender}`, person.detection.box.x, person.detection.box.y - 60);
ctx.fillText(`expression: ${Math.round(100 * expression[0][1])}% ${expression[0][0]}`, person.detection.box.x, person.detection.box.y - 42);
ctx.fillText(`age: ${Math.round(person.age)} years`, person.detection.box.x, person.detection.box.y - 24);
ctx.fillText(`roll:${person.angle.roll.toFixed(3)} pitch:${person.angle.pitch.toFixed(3)} yaw:${person.angle.yaw.toFixed(3)}`, person.detection.box.x, person.detection.box.y - 6);
// draw face points for each face
ctx.globalAlpha = 0.8;
ctx.fillStyle = 'lightblue';
const pointSize = 2;
for (let i = 0; i < person.landmarks.positions.length; i++) {
ctx.beginPath();
ctx.arc(person.landmarks.positions[i].x, person.landmarks.positions[i].y, pointSize, 0, 2 * Math.PI);
// ctx.fillText(`${i}`, person.landmarks.positions[i].x + 4, person.landmarks.positions[i].y + 4);
ctx.fill();
}
}
}
async function detectVideo(video, canvas) {
if (!video || video.paused) return false;
const t0 = performance.now();
faceapi
.detectAllFaces(video, optionsSSDMobileNet)
.withFaceLandmarks()
.withFaceExpressions()
// .withFaceDescriptors()
.withAgeAndGender()
.then((result) => {
const fps = 1000 / (performance.now() - t0);
drawFaces(canvas, result, fps.toLocaleString());
requestAnimationFrame(() => detectVideo(video, canvas));
return true;
})
.catch((err) => {
log(`Detect Error: ${str(err)}`);
return false;
});
return false;
}
// just initialize everything and call main function
async function setupCamera() {
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
if (!video || !canvas) return null;
let msg = '';
log('Setting up camera');
// setup webcam. note that navigator.mediaDevices requires that page is accessed via https
if (!navigator.mediaDevices) {
log('Camera Error: access not supported');
return null;
}
let stream;
const constraints = {
audio: false,
video: { facingMode: 'user', resizeMode: 'crop-and-scale' },
};
if (window.innerWidth > window.innerHeight) constraints.video.width = { ideal: window.innerWidth };
else constraints.video.height = { ideal: window.innerHeight };
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
if (err.name === 'PermissionDeniedError' || err.name === 'NotAllowedError') msg = 'camera permission denied';
else if (err.name === 'SourceUnavailableError') msg = 'camera not available';
log(`Camera Error: ${msg}: ${err.message || err}`);
return null;
}
// @ts-ignore
if (stream) video.srcObject = stream;
else {
log('Camera Error: stream empty');
return null;
}
const track = stream.getVideoTracks()[0];
const settings = track.getSettings();
if (settings.deviceId) delete settings.deviceId;
if (settings.groupId) delete settings.groupId;
if (settings.aspectRatio) settings.aspectRatio = Math.trunc(100 * settings.aspectRatio) / 100;
log(`Camera active: ${track.label}`); // ${str(constraints)}
log(`Camera settings: ${str(settings)}`);
canvas.addEventListener('click', () => {
// @ts-ignore
if (video && video.readyState >= 2) {
// @ts-ignore
if (video.paused) {
// @ts-ignore
video.play();
detectVideo(video, canvas);
} else {
// @ts-ignore
video.pause();
}
}
// @ts-ignore
log(`Camera state: ${video.paused ? 'paused' : 'playing'}`);
});
return new Promise((resolve) => {
video.onloadeddata = async () => {
// @ts-ignore
canvas.width = video.videoWidth;
// @ts-ignore
canvas.height = video.videoHeight;
// @ts-ignore
video.play();
detectVideo(video, canvas);
resolve(true);
};
});
}
async function setupFaceAPI() {
// load face-api models
// log('Models loading');
// await faceapi.nets.tinyFaceDetector.load(modelPath); // using ssdMobilenetv1
await faceapi.nets.ssdMobilenetv1.load(modelPath);
await faceapi.nets.ageGenderNet.load(modelPath);
await faceapi.nets.faceLandmark68Net.load(modelPath);
await faceapi.nets.faceRecognitionNet.load(modelPath);
await faceapi.nets.faceExpressionNet.load(modelPath);
optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence: minScore, maxResults });
// check tf engine state
log(`Models loaded: ${str(faceapi.tf.engine().state.numTensors)} tensors`);
}
async function main() {
// initialize tfjs
log('FaceAPI WebCam Test');
// if you want to use wasm backend location for wasm binaries must be specified
// await faceapi.tf.setWasmPaths(`https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm@${faceapi.tf.version_core}/dist/`);
// await faceapi.tf.setBackend('wasm');
// default is webgl backend
await faceapi.tf.setBackend('webgl');
await faceapi.tf.enableProdMode();
await faceapi.tf.ENV.set('DEBUG', false);
await faceapi.tf.ready();
// check version
log(`Version: FaceAPI ${str(faceapi?.version || '(not loaded)')} TensorFlow/JS ${str(faceapi?.tf?.version_core || '(not loaded)')} Backend: ${str(faceapi?.tf?.getBackend() || '(not loaded)')}`);
// log(`Flags: ${JSON.stringify(faceapi?.tf?.ENV.flags || { tf: 'not loaded' })}`);
await setupFaceAPI();
await setupCamera();
}
// start processing as soon as page is loaded
window.onload = main;