1
- // Copyright (c) 2020-2023 ml5
2
- //
3
- // This software is released under the MIT License.
4
- // https://opensource.org/licenses/MIT
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2020-2024 ml5
4
+ * This software is released under the ml5.js License.
5
+ * https://github.com/ml5js/ml5-next-gen/blob/main/LICENSE.md
6
+ */
5
7
6
- /*
7
- * FaceMesh: Face landmarks tracking in the browser
8
- * Ported and integrated from all the hard work by: https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection
8
+ /**
9
+ * @file HandPose
10
+ *
11
+ * The file contains the main source code of FaceMesh, a pretrained face landmark
12
+ * estimation model that detects and tracks faces and facial features with landmark points.
13
+ * The FaceMesh model is built on top of the face detection model of TensorFlow.
14
+ *
15
+ * TensorFlow Face Detection repo:
16
+ * https://github.com/tensorflow/tfjs-models/tree/master/face-detection
17
+ * ml5.js BodyPose reference documentation:
18
+ * https://docs.ml5js.org/#/reference/facemesh
9
19
*/
10
20
11
21
import * as tf from "@tensorflow/tfjs" ;
@@ -16,24 +26,75 @@ import { mediaReady } from "../utils/imageUtilities";
16
26
import handleOptions from "../utils/handleOptions" ;
17
27
import { handleModelName } from "../utils/handleOptions" ;
18
28
19
- class FaceMesh {
20
- /**
21
- * An options object to configure FaceMesh settings
22
- * @typedef {Object } configOptions
23
- * @property {number } maxFacess - The maximum number of faces to detect. Defaults to 2.
24
- * @property {boolean } refineLandmarks - Refine the ladmarks. Defaults to false.
25
- * @property {boolean } flipHorizontal - Flip the result horizontally. Defaults to false.
26
- * @property {string } runtime - The runtime to use. "mediapipe"(default) or "tfjs".
27
- *
28
- * // For using custom or offline models
29
- * @property {string } solutionPath - The file path or URL to the model.
30
- */
29
+ /**
30
+ * User provided options object for FaceMesh. See config schema below for default and available values.
31
+ * @typedef {Object } configOptions
32
+ * @property {number } [maxFaces] - The maximum number of faces to detect.
33
+ * @property {boolean } [refineLandmarks] - Whether to refine the landmarks.
34
+ * @property {boolean } [flipHorizontal] - Whether to mirror the results.
35
+ * @property {string } [runtime] - The runtime to use.
36
+ * @property {string } [solutionPath] - The file path or URL to the MediaPipe solution. Only
37
+ * for `mediapipe` runtime.
38
+ * @property {string } [detectorModelUrl] - The file path or URL to the detector model. Only for
39
+ * `tfjs` runtime.
40
+ * @property {string } [landmarkModelUrl] - The file path or URL to the landmark model. Only for
41
+ * `tfjs` runtime.
42
+ */
43
+
44
+ /**
45
+ * Schema for initialization options, used by `handleOptions` to
46
+ * validate the user's options object.
47
+ */
48
+ const configSchema = {
49
+ runtime : {
50
+ type : "enum" ,
51
+ enums : [ "mediapipe" , "tfjs" ] ,
52
+ default : "tfjs" ,
53
+ } ,
54
+ maxFaces : {
55
+ type : "number" ,
56
+ min : 1 ,
57
+ default : 1 ,
58
+ } ,
59
+ refineLandmarks : {
60
+ type : "boolean" ,
61
+ default : false ,
62
+ } ,
63
+ solutionPath : {
64
+ type : "string" ,
65
+ default : "https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh" ,
66
+ ignore : ( config ) => config . runtime !== "mediapipe" ,
67
+ } ,
68
+ detectorModelUrl : {
69
+ type : "string" ,
70
+ default : undefined ,
71
+ ignore : ( config ) => config . runtime !== "tfjs" ,
72
+ } ,
73
+ landmarkModelUrl : {
74
+ type : "string" ,
75
+ default : undefined ,
76
+ ignore : ( config ) => config . runtime !== "tfjs" ,
77
+ } ,
78
+ } ;
31
79
80
+ /**
81
+ * Schema for runtime options, used by `handleOptions` to
82
+ * validate the user's options object.
83
+ */
84
+ const runtimeSchema = {
85
+ flipHorizontal : {
86
+ type : "boolean" ,
87
+ alias : "flipped" ,
88
+ default : false ,
89
+ } ,
90
+ } ;
91
+
92
+ class FaceMesh {
32
93
/**
33
- * Create FaceMesh.
94
+ * Creates an instance of FaceMesh.
95
+ * @param {string } [modelName] - The name of the model to use.
34
96
* @param {configOptions } options - An object with options.
35
97
* @param {function } callback - A callback to be called when the model is ready.
36
- *
37
98
* @private
38
99
*/
39
100
constructor ( modelName , options , callback ) {
@@ -43,77 +104,46 @@ class FaceMesh {
43
104
"FaceMesh" ,
44
105
"faceMesh"
45
106
) ;
107
+ /** The underlying TensorFlow.js detector instance.*/
46
108
this . model = null ;
47
- this . config = options ;
109
+ /** The user provided options object. */
110
+ this . userOptions = options ;
111
+ /** The config passed to underlying detector instance during inference. */
48
112
this . runtimeConfig = { } ;
113
+ /** The media source being continuously detected. Only used in continuous mode. */
49
114
this . detectMedia = null ;
115
+ /** The callback function for detection results. Only used in continuous mode. */
50
116
this . detectCallback = null ;
51
-
52
- // flags for detectStart() and detectStop()
53
- this . detecting = false ; // true when detection loop is running
54
- this . signalStop = false ; // true when detectStop() is called and detecting is true
55
- this . prevCall = "" ; // "start" or "stop", used for giving warning messages with detectStart() is called twice in a row
56
-
117
+ /** A flag for continuous mode, set to true when detection loop is running.*/
118
+ this . detecting = false ;
119
+ /** A flag to signal stop to the detection loop.*/
120
+ this . signalStop = false ;
121
+ /** A flag to track the previous call to`detectStart` and `detectStop`. */
122
+ this . prevCall = "" ;
123
+ /** A promise that resolves when the model is ready. */
57
124
this . ready = callCallback ( this . loadModel ( ) , callback ) ;
58
125
}
59
126
60
127
/**
61
- * Load the model and set it to this.model
128
+ * Load the FaceMesh instance.
62
129
* @return {this } the FaceMesh model.
63
- *
64
130
* @private
65
131
*/
66
132
async loadModel ( ) {
67
133
const pipeline = faceLandmarksDetection . SupportedModels . MediaPipeFaceMesh ;
68
- // filter out model config options
134
+ // Filter out model config options
69
135
const modelConfig = handleOptions (
70
- this . config ,
71
- {
72
- runtime : {
73
- type : "enum" ,
74
- enums : [ "mediapipe" , "tfjs" ] ,
75
- default : "tfjs" ,
76
- } ,
77
- maxFaces : {
78
- type : "number" ,
79
- min : 1 ,
80
- default : 1 ,
81
- } ,
82
- refineLandmarks : {
83
- type : "boolean" ,
84
- default : false ,
85
- } ,
86
- solutionPath : {
87
- type : "string" ,
88
- default : "https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh" ,
89
- ignore : ( config ) => config . runtime !== "mediapipe" ,
90
- } ,
91
- detectorModelUrl : {
92
- type : "string" ,
93
- default : undefined ,
94
- ignore : ( config ) => config . runtime !== "tfjs" ,
95
- } ,
96
- landmarkModelUrl : {
97
- type : "string" ,
98
- default : undefined ,
99
- ignore : ( config ) => config . runtime !== "tfjs" ,
100
- } ,
101
- } ,
136
+ this . userOptions ,
137
+ configSchema ,
102
138
"faceMesh"
103
139
) ;
104
-
105
140
this . runtimeConfig = handleOptions (
106
- this . config ,
107
- {
108
- flipHorizontal : {
109
- type : "boolean" ,
110
- alias : "flipped" ,
111
- default : false ,
112
- } ,
113
- } ,
141
+ this . userOptions ,
142
+ runtimeSchema ,
114
143
"faceMesh"
115
144
) ;
116
145
146
+ // Load the model once tfjs is ready
117
147
await tf . ready ( ) ;
118
148
this . model = await faceLandmarksDetection . createDetector (
119
149
pipeline ,
@@ -124,20 +154,21 @@ class FaceMesh {
124
154
}
125
155
126
156
/**
127
- * Asynchronously output a single face prediction result when called
128
- * @param {* } [media] - An HMTL or p5.js image, video, or canvas element to run the prediction on.
129
- * @param {function } [callback] - A callback function to handle the predictions.
130
- * @returns {Promise<Array> } an array of predictions.
157
+ * Asynchronously outputs a single face prediction result when called.
158
+ * @param {any } media - An HTML or p5.js image, video, or canvas element to run the prediction on.
159
+ * @param {function } [callback] - A callback function to handle the detection result.
160
+ * @returns {Promise<Array> } an array of predicted faces.
161
+ * @public
131
162
*/
132
163
async detect ( ...inputs ) {
133
- // Parse out the input parameters
164
+ // Parse the input parameters
134
165
const argumentObject = handleArguments ( ...inputs ) ;
135
166
argumentObject . require (
136
167
"image" ,
137
168
"An html or p5.js image, video, or canvas element argument is required for detect()."
138
169
) ;
139
170
const { image, callback } = argumentObject ;
140
-
171
+ // Run the prediction
141
172
await mediaReady ( image , false ) ;
142
173
const predictions = await this . model . estimateFaces (
143
174
image ,
@@ -150,13 +181,13 @@ class FaceMesh {
150
181
}
151
182
152
183
/**
153
- * Repeatedly output face predictions through a callback function
154
- * @param {* } [ media] - An HMTL or p5.js image, video, or canvas element to run the prediction on.
155
- * @param {function } [callback] - A callback function to handle the predictions .
156
- * @returns { Promise<Array> } an array of predictions.
184
+ * Repeatedly outputs face predictions through a callback function.
185
+ * @param {any } media - An HTML or p5.js image, video, or canvas element to run the prediction on.
186
+ * @param {function } [callback] - A callback function to handle the prediction results .
187
+ * @public
157
188
*/
158
189
detectStart ( ...inputs ) {
159
- // Parse out the input parameters
190
+ // Parse the input parameters
160
191
const argumentObject = handleArguments ( ...inputs ) ;
161
192
argumentObject . require (
162
193
"image" ,
@@ -169,6 +200,7 @@ class FaceMesh {
169
200
this . detectMedia = argumentObject . image ;
170
201
this . detectCallback = argumentObject . callback ;
171
202
203
+ // Set the flags and call the detection loop
172
204
this . signalStop = false ;
173
205
if ( ! this . detecting ) {
174
206
this . detecting = true ;
@@ -183,17 +215,17 @@ class FaceMesh {
183
215
}
184
216
185
217
/**
186
- * Stop the detection loop before next detection loop runs.
218
+ * Stop the continuous detection before next detection loop runs.
219
+ * @public
187
220
*/
188
221
detectStop ( ) {
189
222
if ( this . detecting ) this . signalStop = true ;
190
223
this . prevCall = "stop" ;
191
224
}
192
225
193
226
/**
194
- * Internal function to call estimateFaces in a loop
195
- * Can be started by detectStart() and terminated by detectStop()
196
- *
227
+ * Calls estimateFaces in a loop.
228
+ * Can be started by `detectStart` and terminated by `detectStop`.
197
229
* @private
198
230
*/
199
231
async detectLoop ( ) {
@@ -214,21 +246,22 @@ class FaceMesh {
214
246
}
215
247
216
248
/**
217
- * Return a new array of results with named keypoints added
218
- * @param { Array } faces - the original detection results
219
- * @return {Array } the detection results with named keypoints added
220
- *
249
+ * Return a new array of results with named features added.
250
+ * The keypoints in each named feature is sorted the order of the contour.
251
+ * @param {Array } faces - The original detection results.
252
+ * @return { Array } - The detection results with named keypoints added.
221
253
* @private
222
254
*/
223
255
addKeypoints ( faces ) {
224
256
const contours = faceLandmarksDetection . util . getKeypointIndexByContour (
225
257
faceLandmarksDetection . SupportedModels . MediaPipeFaceMesh
226
258
) ;
259
+ // Add the missing keypoint to the lips contour
227
260
// Remove the following line when the tfjs fix the lips issue
261
+ // https://github.com/tensorflow/tfjs/issues/8221
228
262
if ( contours . lips [ 20 ] !== 291 ) contours . lips . splice ( 20 , 0 , 291 ) ;
229
263
for ( let face of faces ) {
230
264
// Remove the following line when the tfjs fix the lips issue
231
- // https://github.com/tensorflow/tfjs/issues/8221
232
265
face . keypoints [ 291 ] . name = "lips" ;
233
266
for ( let contourLabel in contours ) {
234
267
for ( let keypointIndex of contours [ contourLabel ] ) {
@@ -293,8 +326,11 @@ class FaceMesh {
293
326
}
294
327
295
328
/**
296
- * Factory function that returns a FaceMesh instance
297
- * @returns {Object } A new faceMesh instance
329
+ * Factory function that returns a FaceMesh instance.
330
+ * @param {string } [modelName] - The name of the model to use.
331
+ * @param {configOptions } [options] - A user-defined options object.
332
+ * @param {function } [callback] - A callback to be called when the model is ready.
333
+ * @returns {Object } A new faceMesh instance.
298
334
*/
299
335
const faceMesh = ( ...inputs ) => {
300
336
const { string, options = { } , callback } = handleArguments ( ...inputs ) ;
0 commit comments