Skip to content

Commit 41be997

Browse files
authored
Handpose cleanup (#196)
* update file description and schema * update property comments * tweak comments
1 parent 9d43997 commit 41be997

File tree

2 files changed

+133
-96
lines changed

2 files changed

+133
-96
lines changed

src/BodyPose/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ class BodyPose {
263263

264264
/**
265265
* Asynchronously outputs a single pose prediction result when called.
266-
* @param {any} media - An HMTL or p5.js image, video, or canvas element to run the prediction on.
266+
* @param {any} media - An HTML or p5.js image, video, or canvas element to run the prediction on.
267267
* @param {function} callback - A callback function to handle the predictions.
268268
* @returns {Promise<Array>} an array of poses.
269269
* @public

src/HandPose/index.js

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

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

1121
import * as tf from "@tensorflow/tfjs";
@@ -17,109 +27,131 @@ import { handleModelName } from "../utils/handleOptions";
1727
import { mediaReady } from "../utils/imageUtilities";
1828
import objectRenameKey from "../utils/objectRenameKey";
1929

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

96+
class HandPose {
3597
/**
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.
39102
* @private
40103
*/
41104
constructor(modelName, options, callback) {
105+
/** The underlying model used. */
42106
this.modelName = this.modelName = handleModelName(
43107
modelName,
44108
["MediaPipeHands"],
45109
"MediaPipeHands",
46110
"handPose"
47111
);
112+
/** The underlying TensorFlow.js detector instance.*/
48113
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. */
50117
this.runtimeConfig = {};
118+
/** The media source being continuously detected. Only used in continuous mode. */
51119
this.detectMedia = null;
120+
/** The callback function for detection results. Only used in continuous mode. */
52121
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. */
59129
this.ready = callCallback(this.loadModel(), callback);
60130
}
61131

62132
/**
63-
* Loads the model.
64-
* @return {this} the HandPose model.
133+
* Loads the HandPose instance.
134+
* @return {this} the HandPose instance.
65135
* @private
66136
*/
67137
async loadModel() {
68138
const pipeline = handPoseDetection.SupportedModels.MediaPipeHands;
69-
//filter out model config options
139+
// Filter out initialization config options
70140
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,
106143
"handPose"
107144
);
145+
// Filter out the runtime config options
108146
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,
117149
"handPose"
118150
);
119151

152+
// Load the Tensorflow.js detector instance
120153
await tf.ready();
121154
this.model = await handPoseDetection.createDetector(pipeline, modelConfig);
122-
123155
return this;
124156
}
125157

@@ -132,40 +164,42 @@ class HandPose {
132164
/**
133165
* Asynchronously outputs a single hand landmark detection result when called.
134166
* 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
138171
*/
139172
async detect(...inputs) {
140-
//Parse out the input parameters
173+
//Parse the input parameters
141174
const argumentObject = handleArguments(...inputs);
142175
argumentObject.require(
143176
"image",
144177
"An html or p5.js image, video, or canvas element argument is required for detect()."
145178
);
146179
const { image, callback } = argumentObject;
147-
180+
// Run the detection
148181
await mediaReady(image, false);
149182
const predictions = await this.model.estimateHands(
150183
image,
151184
this.runtimeConfig
152185
);
153-
// Modify the prediction result to make it more user-friendly
186+
// Modify the raw tfjs output to make it more user-friendly
154187
let result = predictions;
155188
this.renameScoreToConfidence(result);
156189
this.addKeypoints(result);
157-
190+
// Output the result via callback and/or promise
158191
if (typeof callback === "function") callback(result);
159192
return result;
160193
}
161194

162195
/**
163196
* 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
166200
*/
167201
detectStart(...inputs) {
168-
// Parse out the input parameters
202+
// Parse the input parameters
169203
const argumentObject = handleArguments(...inputs);
170204
argumentObject.require(
171205
"image",
@@ -181,6 +215,7 @@ class HandPose {
181215
this.signalStop = false;
182216
if (!this.detecting) {
183217
this.detecting = true;
218+
// Call the internal detection loop
184219
this.detectLoop();
185220
}
186221
if (this.prevCall === "start") {
@@ -193,6 +228,7 @@ class HandPose {
193228

194229
/**
195230
* Stops the detection loop before next detection loop runs.
231+
* @public
196232
*/
197233
detectStop() {
198234
if (this.detecting) this.signalStop = true;
@@ -201,7 +237,7 @@ class HandPose {
201237

202238
/**
203239
* 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`.
205241
* @private
206242
*/
207243
async detectLoop() {
@@ -225,8 +261,9 @@ class HandPose {
225261
}
226262

227263
/**
228-
* Renames the score key to confidence in the detection results.
264+
* Renames the `score` key to `confidence` in the detection results.
229265
* @param {Object[]} hands - The detection results.
266+
* @private
230267
*/
231268
renameScoreToConfidence(hands) {
232269
hands.forEach((hand) => {
@@ -235,14 +272,12 @@ class HandPose {
235272
}
236273

237274
/**
238-
* Returns a new array of results with named keypoints added.
275+
* Add the named keypoints to the detection results.
239276
* @param {Array} hands - the original detection results.
240-
* @return {Array} the detection results with named keypoints added.
241-
*
242277
* @private
243278
*/
244279
addKeypoints(hands) {
245-
const result = hands.map((hand) => {
280+
hands = hands.map((hand) => {
246281
for (let i = 0; i < hand.keypoints.length; i++) {
247282
let keypoint = hand.keypoints[i];
248283
let keypoint3D = hand.keypoints3D[i];
@@ -256,12 +291,14 @@ class HandPose {
256291
}
257292
return hand;
258293
});
259-
return result;
260294
}
261295
}
262296

263297
/**
264298
* 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.
265302
* @returns {HandPose} A new handPose instance.
266303
*/
267304
const handPose = (...inputs) => {

0 commit comments

Comments
 (0)