1
- // Copyright (c) 2020-2024 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
- * HandPose: Palm detector and hand-skeleton finger tracking in the browser
8
- * Ported and integrated from all the hard work by: https://github.com/tensorflow/tfjs-models/tree/master/hand-pose-detection
8
+ /**
9
+ * @file HandPose
10
+ *
11
+ * The file contains the main code of HandPose, a pretrained hand landmark
12
+ * estimation model that can estimate poses and track key body parts in real-time.
13
+ * The HandPose model is built on top of the hand detection model of TensorFlow.
14
+ *
15
+ * TensorFlow Hand Pose Detection repo:
16
+ * https://github.com/tensorflow/tfjs-models/tree/master/hand-pose-detection
17
+ * ml5.js BodyPose reference documentation:
18
+ * https://docs.ml5js.org/#/reference/handpose
9
19
*/
10
20
11
21
import * as tf from "@tensorflow/tfjs" ;
@@ -17,109 +27,131 @@ import { handleModelName } from "../utils/handleOptions";
17
27
import { mediaReady } from "../utils/imageUtilities" ;
18
28
import objectRenameKey from "../utils/objectRenameKey" ;
19
29
20
- class HandPose {
21
- /**
22
- * An object for configuring HandPose options.
23
- * @typedef {Object } configOptions
24
- * @property {number } maxHands - Optional. The maximum number of hands to detect. Default: 2.
25
- * @property {string } modelType - Optional. The type of model to use: "lite" or "full". Default: "full".
26
- * @property {boolean } flipHorizontal - Optional. Flip the result data horizontally. Default: false.
27
- * @property {string } runtime - Optional. The runtime of the model: "mediapipe" or "tfjs". Default: "mediapipe".
28
- *
29
- * // For using custom or offline models
30
- * @property {string } solutionPath - Optional. The file path or URL to the model. Only used when using "mediapipe" runtime.
31
- * @property {string } detectorModelUrl - Optional. The file path or URL to the hand detector model. Only used when using "tfjs" runtime.
32
- * @property {string } landmarkModelUrl - Optional. The file path or URL to the hand landmark model Only used when using "tfjs" runtime.
33
- */
30
+ /**
31
+ * User provided options object for HandPose. See config schema below for default and available values.
32
+ * @typedef {object } configOptions
33
+ * @property {number } [maxHands] - The maximum number of hands to detect.
34
+ * @property {string } [modelType] - The type of model to use.
35
+ * @property {boolean } [flipHorizontal] - Whether to mirror the landmark results.
36
+ * @property {string } [runtime] - The runtime of the model.
37
+ * @property {string } [solutionPath] - The file path or URL to mediaPipe solution. Only for
38
+ * `mediapipe` runtime.
39
+ * @property {string } [detectorModelUrl] - The file path or URL to the hand detector model. Only
40
+ * for `tfjs` runtime.
41
+ * @property {string } [landmarkModelUrl] - The file path or URL to the hand landmark model. Only
42
+ * for `tfjs` runtime.
43
+ */
44
+
45
+ /**
46
+ * Schema for initialization options, used by `handleOptions` to
47
+ * validate the user's options object.
48
+ */
49
+ const configSchema = {
50
+ maxHands : {
51
+ type : "number" ,
52
+ min : 1 ,
53
+ max : 2147483647 ,
54
+ integer : true ,
55
+ default : 2 ,
56
+ } ,
57
+ runtime : {
58
+ type : "enum" ,
59
+ enums : [ "mediapipe" , "tfjs" ] ,
60
+ default : "tfjs" ,
61
+ } ,
62
+ modelType : {
63
+ type : "enum" ,
64
+ enums : [ "lite" , "full" ] ,
65
+ default : "full" ,
66
+ } ,
67
+ solutionPath : {
68
+ type : "string" ,
69
+ default : "https://cdn.jsdelivr.net/npm/@mediapipe/hands" ,
70
+ ignore : ( config ) => config . runtime !== "mediapipe" ,
71
+ } ,
72
+ detectorModelUrl : {
73
+ type : "string" ,
74
+ default : undefined ,
75
+ ignore : ( config ) => config . runtime !== "tfjs" ,
76
+ } ,
77
+ landmarkModelUrl : {
78
+ type : "string" ,
79
+ default : undefined ,
80
+ ignore : ( config ) => config . runtime !== "tfjs" ,
81
+ } ,
82
+ } ;
83
+
84
+ /**
85
+ * Schema for runtime options, used by `handleOptions` to
86
+ * validate the user's options object.
87
+ */
88
+ const runtimeConfigSchema = {
89
+ flipHorizontal : {
90
+ type : "boolean" ,
91
+ alias : "flipped" ,
92
+ default : false ,
93
+ } ,
94
+ } ;
34
95
96
+ class HandPose {
35
97
/**
36
- * Creates HandPose.
37
- * @param {configOptions } options - An object containing HandPose configuration options.
38
- * @param {function } callback - A callback to be called when the model is ready.
98
+ * Creates an instance of HandPose model.
99
+ * @param {string } [modelName] - The underlying model to use, must be `MediaPipeHands` or undefined.
100
+ * @param {configOptions } options -An options object for the model.
101
+ * @param {function } callback - A callback function that is called once the model has been loaded.
39
102
* @private
40
103
*/
41
104
constructor ( modelName , options , callback ) {
105
+ /** The underlying model used. */
42
106
this . modelName = this . modelName = handleModelName (
43
107
modelName ,
44
108
[ "MediaPipeHands" ] ,
45
109
"MediaPipeHands" ,
46
110
"handPose"
47
111
) ;
112
+ /** The underlying TensorFlow.js detector instance.*/
48
113
this . model = null ;
49
- this . config = options ;
114
+ /** The user provided options object. */
115
+ this . userOptions = options ;
116
+ /** The config passed to underlying detector instance during inference. */
50
117
this . runtimeConfig = { } ;
118
+ /** The media source being continuously detected. Only used in continuous mode. */
51
119
this . detectMedia = null ;
120
+ /** The callback function for detection results. Only used in continuous mode. */
52
121
this . detectCallback = null ;
53
-
54
- // flags for detectStart() and detectStop()
55
- this . detecting = false ; // True when detection loop is running
56
- this . signalStop = false ; // Signal to stop the loop
57
- this . prevCall = "" ; // Track previous call to detectStart() or detectStop()
58
-
122
+ /** A flag for continuous mode, set to true when detection loop is running.*/
123
+ this . detecting = false ;
124
+ /** A flag to signal stop to the detection loop.*/
125
+ this . signalStop = false ;
126
+ /** A flag to track the previous call to`detectStart` and `detectStop`. */
127
+ this . prevCall = "" ;
128
+ /** A promise that resolves when the model is ready. */
59
129
this . ready = callCallback ( this . loadModel ( ) , callback ) ;
60
130
}
61
131
62
132
/**
63
- * Loads the model .
64
- * @return {this } the HandPose model .
133
+ * Loads the HandPose instance .
134
+ * @return {this } the HandPose instance .
65
135
* @private
66
136
*/
67
137
async loadModel ( ) {
68
138
const pipeline = handPoseDetection . SupportedModels . MediaPipeHands ;
69
- //filter out model config options
139
+ // Filter out initialization config options
70
140
const modelConfig = handleOptions (
71
- this . config ,
72
- {
73
- maxHands : {
74
- type : "number" ,
75
- min : 1 ,
76
- max : 2147483647 ,
77
- integer : true ,
78
- default : 2 ,
79
- } ,
80
- runtime : {
81
- type : "enum" ,
82
- enums : [ "mediapipe" , "tfjs" ] ,
83
- default : "tfjs" ,
84
- } ,
85
- modelType : {
86
- type : "enum" ,
87
- enums : [ "lite" , "full" ] ,
88
- default : "full" ,
89
- } ,
90
- solutionPath : {
91
- type : "string" ,
92
- default : "https://cdn.jsdelivr.net/npm/@mediapipe/hands" ,
93
- ignore : ( config ) => config . runtime !== "mediapipe" ,
94
- } ,
95
- detectorModelUrl : {
96
- type : "string" ,
97
- default : undefined ,
98
- ignore : ( config ) => config . runtime !== "tfjs" ,
99
- } ,
100
- landmarkModelUrl : {
101
- type : "string" ,
102
- default : undefined ,
103
- ignore : ( config ) => config . runtime !== "tfjs" ,
104
- } ,
105
- } ,
141
+ this . userOptions ,
142
+ configSchema ,
106
143
"handPose"
107
144
) ;
145
+ // Filter out the runtime config options
108
146
this . runtimeConfig = handleOptions (
109
- this . config ,
110
- {
111
- flipHorizontal : {
112
- type : "boolean" ,
113
- alias : "flipped" ,
114
- default : false ,
115
- } ,
116
- } ,
147
+ this . userOptions ,
148
+ runtimeConfigSchema ,
117
149
"handPose"
118
150
) ;
119
151
152
+ // Load the Tensorflow.js detector instance
120
153
await tf . ready ( ) ;
121
154
this . model = await handPoseDetection . createDetector ( pipeline , modelConfig ) ;
122
-
123
155
return this ;
124
156
}
125
157
@@ -132,40 +164,42 @@ class HandPose {
132
164
/**
133
165
* Asynchronously outputs a single hand landmark detection result when called.
134
166
* Supports both callback and promise.
135
- * @param {* } [media] - An HMTL or p5.js image, video, or canvas element to run the detection on.
136
- * @param {gotHands } [callback] - Optional. A callback to handle the hand detection result.
137
- * @returns {Promise<Array> } The detection result.
167
+ * @param {any } media - An HTML or p5.js image, video, or canvas element to run the detection on.
168
+ * @param {gotHands } [callback] - A callback to handle the hand detection result.
169
+ * @returns {Promise<Array> } An array of hand detection results.
170
+ * @public
138
171
*/
139
172
async detect ( ...inputs ) {
140
- //Parse out the input parameters
173
+ //Parse the input parameters
141
174
const argumentObject = handleArguments ( ...inputs ) ;
142
175
argumentObject . require (
143
176
"image" ,
144
177
"An html or p5.js image, video, or canvas element argument is required for detect()."
145
178
) ;
146
179
const { image, callback } = argumentObject ;
147
-
180
+ // Run the detection
148
181
await mediaReady ( image , false ) ;
149
182
const predictions = await this . model . estimateHands (
150
183
image ,
151
184
this . runtimeConfig
152
185
) ;
153
- // Modify the prediction result to make it more user-friendly
186
+ // Modify the raw tfjs output to make it more user-friendly
154
187
let result = predictions ;
155
188
this . renameScoreToConfidence ( result ) ;
156
189
this . addKeypoints ( result ) ;
157
-
190
+ // Output the result via callback and/or promise
158
191
if ( typeof callback === "function" ) callback ( result ) ;
159
192
return result ;
160
193
}
161
194
162
195
/**
163
196
* Repeatedly outputs hand predictions through a callback function.
164
- * @param {* } [media] - An HMTL or p5.js image, video, or canvas element to run the prediction on.
165
- * @param {gotHands } [callback] - A callback to handle the hand detection results.
197
+ * @param {any } media - An HTML or p5.js image, video, or canvas element to run the prediction on.
198
+ * @param {gotHands } callback - A callback to handle the hand detection results.
199
+ * @public
166
200
*/
167
201
detectStart ( ...inputs ) {
168
- // Parse out the input parameters
202
+ // Parse the input parameters
169
203
const argumentObject = handleArguments ( ...inputs ) ;
170
204
argumentObject . require (
171
205
"image" ,
@@ -181,6 +215,7 @@ class HandPose {
181
215
this . signalStop = false ;
182
216
if ( ! this . detecting ) {
183
217
this . detecting = true ;
218
+ // Call the internal detection loop
184
219
this . detectLoop ( ) ;
185
220
}
186
221
if ( this . prevCall === "start" ) {
@@ -193,6 +228,7 @@ class HandPose {
193
228
194
229
/**
195
230
* Stops the detection loop before next detection loop runs.
231
+ * @public
196
232
*/
197
233
detectStop ( ) {
198
234
if ( this . detecting ) this . signalStop = true ;
@@ -201,7 +237,7 @@ class HandPose {
201
237
202
238
/**
203
239
* Calls estimateHands in a loop.
204
- * Can be started by detectStart() and terminated by detectStop() .
240
+ * Can be started by ` detectStart` and terminated by ` detectStop` .
205
241
* @private
206
242
*/
207
243
async detectLoop ( ) {
@@ -225,8 +261,9 @@ class HandPose {
225
261
}
226
262
227
263
/**
228
- * Renames the score key to confidence in the detection results.
264
+ * Renames the ` score` key to ` confidence` in the detection results.
229
265
* @param {Object[] } hands - The detection results.
266
+ * @private
230
267
*/
231
268
renameScoreToConfidence ( hands ) {
232
269
hands . forEach ( ( hand ) => {
@@ -235,14 +272,12 @@ class HandPose {
235
272
}
236
273
237
274
/**
238
- * Returns a new array of results with named keypoints added .
275
+ * Add the named keypoints to the detection results .
239
276
* @param {Array } hands - the original detection results.
240
- * @return {Array } the detection results with named keypoints added.
241
- *
242
277
* @private
243
278
*/
244
279
addKeypoints ( hands ) {
245
- const result = hands . map ( ( hand ) => {
280
+ hands = hands . map ( ( hand ) => {
246
281
for ( let i = 0 ; i < hand . keypoints . length ; i ++ ) {
247
282
let keypoint = hand . keypoints [ i ] ;
248
283
let keypoint3D = hand . keypoints3D [ i ] ;
@@ -256,12 +291,14 @@ class HandPose {
256
291
}
257
292
return hand ;
258
293
} ) ;
259
- return result ;
260
294
}
261
295
}
262
296
263
297
/**
264
298
* Factory function that returns a new HandPose instance.
299
+ * @param {string } [modelName] - The underlying model to use, must be `MediaPipeHands`
300
+ * @param {configOptions } [options] - A user-defined options object for the model.
301
+ * @param {function } [callback] - A callback function that is called once the model has been loaded.
265
302
* @returns {HandPose } A new handPose instance.
266
303
*/
267
304
const handPose = ( ...inputs ) => {
0 commit comments