Skip to content

Commit 9aff062

Browse files
authored
Facemesh cleanup (#202)
* Update file header * refactor config schema * update comments and function jsdoc
1 parent 41be997 commit 9aff062

File tree

1 file changed

+130
-94
lines changed

1 file changed

+130
-94
lines changed

src/FaceMesh/index.js

Lines changed: 130 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
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+
*/
57

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
919
*/
1020

1121
import * as tf from "@tensorflow/tfjs";
@@ -16,24 +26,75 @@ import { mediaReady } from "../utils/imageUtilities";
1626
import handleOptions from "../utils/handleOptions";
1727
import { handleModelName } from "../utils/handleOptions";
1828

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+
};
3179

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 {
3293
/**
33-
* Create FaceMesh.
94+
* Creates an instance of FaceMesh.
95+
* @param {string} [modelName] - The name of the model to use.
3496
* @param {configOptions} options - An object with options.
3597
* @param {function} callback - A callback to be called when the model is ready.
36-
*
3798
* @private
3899
*/
39100
constructor(modelName, options, callback) {
@@ -43,77 +104,46 @@ class FaceMesh {
43104
"FaceMesh",
44105
"faceMesh"
45106
);
107+
/** The underlying TensorFlow.js detector instance.*/
46108
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. */
48112
this.runtimeConfig = {};
113+
/** The media source being continuously detected. Only used in continuous mode. */
49114
this.detectMedia = null;
115+
/** The callback function for detection results. Only used in continuous mode. */
50116
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. */
57124
this.ready = callCallback(this.loadModel(), callback);
58125
}
59126

60127
/**
61-
* Load the model and set it to this.model
128+
* Load the FaceMesh instance.
62129
* @return {this} the FaceMesh model.
63-
*
64130
* @private
65131
*/
66132
async loadModel() {
67133
const pipeline = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;
68-
// filter out model config options
134+
// Filter out model config options
69135
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,
102138
"faceMesh"
103139
);
104-
105140
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,
114143
"faceMesh"
115144
);
116145

146+
// Load the model once tfjs is ready
117147
await tf.ready();
118148
this.model = await faceLandmarksDetection.createDetector(
119149
pipeline,
@@ -124,20 +154,21 @@ class FaceMesh {
124154
}
125155

126156
/**
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
131162
*/
132163
async detect(...inputs) {
133-
// Parse out the input parameters
164+
// Parse the input parameters
134165
const argumentObject = handleArguments(...inputs);
135166
argumentObject.require(
136167
"image",
137168
"An html or p5.js image, video, or canvas element argument is required for detect()."
138169
);
139170
const { image, callback } = argumentObject;
140-
171+
// Run the prediction
141172
await mediaReady(image, false);
142173
const predictions = await this.model.estimateFaces(
143174
image,
@@ -150,13 +181,13 @@ class FaceMesh {
150181
}
151182

152183
/**
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
157188
*/
158189
detectStart(...inputs) {
159-
// Parse out the input parameters
190+
// Parse the input parameters
160191
const argumentObject = handleArguments(...inputs);
161192
argumentObject.require(
162193
"image",
@@ -169,6 +200,7 @@ class FaceMesh {
169200
this.detectMedia = argumentObject.image;
170201
this.detectCallback = argumentObject.callback;
171202

203+
// Set the flags and call the detection loop
172204
this.signalStop = false;
173205
if (!this.detecting) {
174206
this.detecting = true;
@@ -183,17 +215,17 @@ class FaceMesh {
183215
}
184216

185217
/**
186-
* Stop the detection loop before next detection loop runs.
218+
* Stop the continuous detection before next detection loop runs.
219+
* @public
187220
*/
188221
detectStop() {
189222
if (this.detecting) this.signalStop = true;
190223
this.prevCall = "stop";
191224
}
192225

193226
/**
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`.
197229
* @private
198230
*/
199231
async detectLoop() {
@@ -214,21 +246,22 @@ class FaceMesh {
214246
}
215247

216248
/**
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.
221253
* @private
222254
*/
223255
addKeypoints(faces) {
224256
const contours = faceLandmarksDetection.util.getKeypointIndexByContour(
225257
faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh
226258
);
259+
// Add the missing keypoint to the lips contour
227260
// Remove the following line when the tfjs fix the lips issue
261+
// https://github.com/tensorflow/tfjs/issues/8221
228262
if (contours.lips[20] !== 291) contours.lips.splice(20, 0, 291);
229263
for (let face of faces) {
230264
// Remove the following line when the tfjs fix the lips issue
231-
// https://github.com/tensorflow/tfjs/issues/8221
232265
face.keypoints[291].name = "lips";
233266
for (let contourLabel in contours) {
234267
for (let keypointIndex of contours[contourLabel]) {
@@ -293,8 +326,11 @@ class FaceMesh {
293326
}
294327

295328
/**
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.
298334
*/
299335
const faceMesh = (...inputs) => {
300336
const { string, options = {}, callback } = handleArguments(...inputs);

0 commit comments

Comments
 (0)