diff --git a/examples/Bodypose-single-image/data/runner.jpg b/examples/Bodypose-single-image/data/runner.jpg new file mode 100644 index 00000000..25df5133 Binary files /dev/null and b/examples/Bodypose-single-image/data/runner.jpg differ diff --git a/examples/Bodypose-single-image/index.html b/examples/Bodypose-single-image/index.html new file mode 100644 index 00000000..9ea888c4 --- /dev/null +++ b/examples/Bodypose-single-image/index.html @@ -0,0 +1,42 @@ + + + + + + ml5.js BodyPose Detection Example + + + + + +

BodyPose example on image with single-person detection

+

Loading model...

+
+ + + + +
+ + +

image by Funk Dooby via Wikimedia

+ + + diff --git a/examples/Bodypose-single-image/sketch.js b/examples/Bodypose-single-image/sketch.js new file mode 100644 index 00000000..05e830c8 --- /dev/null +++ b/examples/Bodypose-single-image/sketch.js @@ -0,0 +1,111 @@ +// declare variables img, bodyPose, and poses in the global scope +// so that we can access them inside the `draw()` function + +/** @type {HTMLImageElement} - the input image */ +let img; + +/** @type {Object} the loaded ml5 model */ +let bodyPose; + +/** @type {Array} - the poses detected by the model */ +let poses; + + +// Preload assets - setup() will not run until the loading is complete. +function preload() { + // Load an image for pose detection + img = loadImage('data/runner.jpg'); + // Load the ml5 model + bodyPose = ml5.bodyPose(); +} + +function setup() { + // Draw the image to the canvas + createCanvas(img.width, img.height, document.querySelector('canvas')); + image(img, 0, 0); + // Do not need to draw on every frame + noLoop(); + // Update the status + select('#status').html('Model Loaded'); + // Detect poses in the image + bodyPose.detect(img, onPose); + // Draw again when changing checkboxes + // TODO: can use p5 function once this fix is published - https://github.com/processing/p5.js/pull/6838 + // select("form").changed(redraw); + document.querySelector('form').addEventListener('change', redraw); +} + +// Function to run when the model detects poses. +function onPose(result) { + // Update the status + select("#status").html('Pose Detected'); + // Store the poses + poses = result; + // Initiate the drawing + redraw(); +} + +// p5 draw function +function draw() { + // Need to reset the canvas by drawing the image again. + // In order to "undraw" when deselecting checkboxes. + image(img, 0, 0); + + // If there are no poses, we are done. + if (!poses) { + return; + } + + // Draw the correct layers based on the current checkboxes. + const showSkeleton = select("#skeleton").checked(); + const showKeypoints = select("#keypoints").checked(); + const showLabels = select("#labels").checked(); + const showBox = select("#box").checked(); + + // Loop through all the poses detected + poses.forEach(pose => { + // For each pose detected, loop through all body connections on the skeleton + if (showSkeleton) { + pose.skeleton?.forEach(connection => { + // Each connection is an array of two parts + const [partA, partB] = connection; + // Draw a line between the two parts + stroke(255); + strokeWeight(2); + line(partA.x, partA.y, partB.x, partB.y); + }); + } + // For each pose detected, draw the bounding box rectangle + if (showBox) { + console.log('drawing box', pose.box); + const boundingBox = pose.box; + stroke(255); + noFill(); + strokeWeight(2); + rect(boundingBox.xMin * width, boundingBox.yMin * height, boundingBox.width * width, boundingBox.height * height); + } + // For each pose detected, loop through all the keypoints + // A keypoint is an object describing a body part (like rightArm or leftShoulder) + pose.keypoints.forEach(keypoint => { + // Only draw an ellipse is the pose probability is bigger than 0.2 + if (keypoint.score > 0.2) { + if (showLabels) { + // Line from part to label + stroke(60); + strokeWeight(1); + line(keypoint.x, keypoint.y, keypoint.x + 10, keypoint.y); + // Write the name of the part + textAlign(LEFT, CENTER); + text(keypoint.name, keypoint.x + 10, keypoint.y); + } + if (showKeypoints) { + // Draw ellipse over part + fill(255); + stroke(20); + strokeWeight(4); + ellipse(round(keypoint.x), round(keypoint.y), 8, 8); + } + } + }); + }); +} diff --git a/src/BodyPose/index.js b/src/BodyPose/index.js index f8130834..92041cb6 100644 --- a/src/BodyPose/index.js +++ b/src/BodyPose/index.js @@ -48,9 +48,6 @@ class BodyPose { * @private */ constructor(modelName = "MoveNet", options, callback) { - // for compatibility with p5's preload() - if (this.p5PreLoadExists()) window._incrementPreload(); - this.modelName = modelName; this.model = null; this.config = options; @@ -128,9 +125,6 @@ class BodyPose { await tf.ready(); this.model = await poseDetection.createDetector(pipeline, modelConfig); - // for compatibility with p5's preload() - if (this.p5PreLoadExists) window._decrementPreload(); - return this; } @@ -250,20 +244,6 @@ class BodyPose { }); return result; } - - /** - * Checks if p5.js' preload() function is present. - * @returns {boolean} true if preload() exists. - * @private - */ - p5PreLoadExists() { - if (typeof window === "undefined") return false; - if (typeof window.p5 === "undefined") return false; - if (typeof window.p5.prototype === "undefined") return false; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return false; - return true; - } } /** diff --git a/src/BodySegmentation/index.js b/src/BodySegmentation/index.js index 43754850..ff144772 100644 --- a/src/BodySegmentation/index.js +++ b/src/BodySegmentation/index.js @@ -23,9 +23,6 @@ class BodySegmentation { * @param {function} [callback] - A callback to be called when the model is ready. */ constructor(modelName, options, callback) { - // for compatibility with p5's preload() - if (this.p5PreLoadExists()) window._incrementPreload(); - this.modelName = modelName; this.video = video; this.model = null; @@ -116,9 +113,6 @@ class BodySegmentation { modelConfig ); - // for compatibility with p5's preload() - if (this.p5PreLoadExists) window._decrementPreload(); - return this; } /** @@ -269,21 +263,6 @@ class BodySegmentation { return imageData; } } - - /** - * Check if p5.js' preload() function is present - * @returns {boolean} true if preload() exists - * - * @private - */ - p5PreLoadExists() { - if (typeof window === "undefined") return false; - if (typeof window.p5 === "undefined") return false; - if (typeof window.p5.prototype === "undefined") return false; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return false; - return true; - } } /** diff --git a/src/FaceMesh/index.js b/src/FaceMesh/index.js index 467fabcf..3b6e517a 100644 --- a/src/FaceMesh/index.js +++ b/src/FaceMesh/index.js @@ -35,9 +35,6 @@ class FaceMesh { * @private */ constructor(options, callback) { - // for compatibility with p5's preload() - if (this.p5PreLoadExists()) window._incrementPreload(); - this.model = null; this.config = options; this.runtimeConfig = {}; @@ -77,9 +74,6 @@ class FaceMesh { modelConfig ); - // for compatibility with p5's preload() - if (this.p5PreLoadExists) window._decrementPreload(); - return this; } @@ -246,21 +240,6 @@ class FaceMesh { } return faces; } - - /** - * Check if p5.js' preload() function is present - * @returns {boolean} true if preload() exists - * - * @private - */ - p5PreLoadExists() { - if (typeof window === "undefined") return false; - if (typeof window.p5 === "undefined") return false; - if (typeof window.p5.prototype === "undefined") return false; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return false; - return true; - } } /** diff --git a/src/HandPose/index.js b/src/HandPose/index.js index 388d98bf..1ce3f04b 100644 --- a/src/HandPose/index.js +++ b/src/HandPose/index.js @@ -36,9 +36,6 @@ class HandPose { * @private */ constructor(options, callback) { - // for compatibility with p5's preload() - if (this.p5PreLoadExists()) window._incrementPreload(); - this.model = null; this.config = options; this.runtimeConfig = {}; @@ -78,9 +75,6 @@ class HandPose { await tf.ready(); this.model = await handPoseDetection.createDetector(pipeline, modelConfig); - // for compatibility with p5's preload() - if (this.p5PreLoadExists) window._decrementPreload(); - return this; } @@ -203,20 +197,6 @@ class HandPose { }); return result; } - - /** - * Check if p5.js' preload() function is present in the current environment. - * @returns {boolean} True if preload() exists. False otherwise. - * @private - */ - p5PreLoadExists() { - if (typeof window === "undefined") return false; - if (typeof window.p5 === "undefined") return false; - if (typeof window.p5.prototype === "undefined") return false; - if (typeof window.p5.prototype.registerPreloadMethod === "undefined") - return false; - return true; - } } /** diff --git a/src/NeuralNetwork/index.js b/src/NeuralNetwork/index.js index 00dfd420..5b004cfb 100644 --- a/src/NeuralNetwork/index.js +++ b/src/NeuralNetwork/index.js @@ -124,7 +124,7 @@ class DiyNeuralNetwork { // will take a URL to model.json, an object, or files array this.ready = this.load(this.options.modelUrl, callback); } else { - this.ready = true; + this.ready = Promise.resolve(this); } } diff --git a/src/Sentiment/index.js b/src/Sentiment/index.js index 0f67c5bf..ee3aa922 100644 --- a/src/Sentiment/index.js +++ b/src/Sentiment/index.js @@ -51,8 +51,8 @@ class Sentiment { */ constructor(modelName, callback) { /** - * Boolean value that specifies if the model has loaded. - * @type {boolean} + * Promise that resolves when the model has loaded. + * @type {Promise} * @public */ this.ready = callCallback(this.loadModel(modelName), callback); diff --git a/src/index.js b/src/index.js index 3fab78d3..227c5ab5 100644 --- a/src/index.js +++ b/src/index.js @@ -10,27 +10,26 @@ import setBackend from "./utils/setBackend"; import bodyPix from "./BodySegmentation"; import communityStatement from "./utils/communityStatement"; import imageClassifier from "./ImageClassifier"; -import preloadRegister from "./utils/p5PreloadHelper"; const withPreload = { + bodyPix, + bodyPose, + faceMesh, + handPose, imageClassifier, + neuralNetwork, + sentiment, }; +const ml5 = Object.assign({ p5Utils }, withPreload, { + tf, + tfvis, + setBackend, + setP5: p5Utils.setP5.bind(p5Utils), +}); -export default Object.assign( - { p5Utils }, - preloadRegister(withPreload), - { - tf, - tfvis, - neuralNetwork, - handPose, - sentiment, - faceMesh, - bodyPose, - setBackend, - bodyPix, - } -); +p5Utils.shouldPreload(ml5, Object.keys(withPreload)); communityStatement(); + +export default ml5; diff --git a/src/utils/p5PreloadHelper.js b/src/utils/p5PreloadHelper.js deleted file mode 100644 index cba16d33..00000000 --- a/src/utils/p5PreloadHelper.js +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2019 ml5 -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - - -/** - * a list to store all functions to hook p5 preload - * @param {obj} object or prototype to wrap with - * @returns obj - */ -export default function registerPreload(obj) { - if (typeof window === 'undefined') return obj; - if (typeof window.p5 === 'undefined') return obj; - if (typeof window.p5.prototype === 'undefined') return obj; - if (typeof window.p5.prototype.registerPreloadMethod === 'undefined') return obj; - - const preloadFn = obj; - Object.keys(obj).forEach((key) => { - const fn = obj[key]; - - preloadFn[key] = function preloads(...args) { - let originCallback = null; - let argLen = args.length; - if (typeof args[argLen - 1] === 'function') { - // find callback function attached - originCallback = args[argLen - 1]; - argLen -= 1; - } - return fn.apply(obj, [...args.slice(0, argLen), function doingPreloads() { - const targetPreloadFn = '_decrementPreload'; - try { - if (originCallback) originCallback(); - } catch (err) { - console.error(err); - } - if (window[targetPreloadFn]) return window[targetPreloadFn](); - return null; - }]); - }; - window.p5.prototype.registerPreloadMethod(`${key}`, obj); - }); - - return obj; -} diff --git a/src/utils/p5Utils.js b/src/utils/p5Utils.js index f1ba10f5..bc396bec 100644 --- a/src/utils/p5Utils.js +++ b/src/utils/p5Utils.js @@ -1,50 +1,178 @@ -// Copyright (c) 2018 ml5 +// Copyright (c) 2018 - 2024 ml5 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT +function isP5Constructor(source) { + return Boolean( + source && + typeof source === "function" && + source.prototype && + source.prototype.registerMethod + ); +} + +function isP5Extensions(source) { + return Boolean(source && typeof source.loadImage === "function"); +} + class P5Util { constructor() { + /** + * @type {boolean} + */ + this.didSetupPreload = false; + /** + * The `p5` variable, which can be instantiated via `new p5()` and has a `.prototype` property. + * In browser environments this is `window.p5`. + * When loading p5 via npm it must be manually provided using `ml5.setP5()`. + */ + this.p5Constructor = undefined; + /** + * Object with all of the constants (HSL etc.) and methods like loadImage(). + * In global mode, this is the Window object. + */ + this.p5Extensions = undefined; + + /** + * Keep a reference to the arguments of `shouldPreload()` so that preloads + * can be set up after the fact if p5 becomes available. + */ + this.ml5Library = undefined; + this.methodsToPreload = []; + + // Check for p5 on the window. + this.findAndSetP5(); + } + + /** + * @private + * Check the window or globalThis for p5. + * Can run this repeatedly in case p5 is loaded after ml5 is loaded. + */ + findAndSetP5() { + let source; if (typeof window !== "undefined") { - /** - * Store the window as a private property regardless of whether p5 is present. - * Can also set this property by calling method setP5Instance(). - * @property {Window | p5 | {p5: p5} | undefined} m_p5Instance - * @private - */ - this.m_p5Instance = window; + source = window; + } else if (typeof globalThis !== "undefined") { + source = globalThis; + } + + if (!source) return; + + if (isP5Constructor(source.p5)) { + this.p5Constructor = source.p5; + this.registerPreloads(); + } + if (isP5Extensions(source)) { + this.p5Extensions = source; } } /** - * Set p5 instance globally in order to enable p5 features throughout ml5. - * Call this function with the p5 instance when using p5 in instance mode. - * @param {p5 | {p5: p5}} p5Instance + * @public + * Set p5 in order to enable p5 features throughout ml5. + * This manual setup is only necessary when importing `p5` as a module + * rather than loading it on the window. + * Can be used in ml5 unit tests to check p5 behavior. + * + * @example + * import p5 from "p5"; + * import ml5 "ml5"; + * + * ml5.setP5(p5); + * + * @param {p5} p5 */ - setP5Instance(p5Instance) { - this.m_p5Instance = p5Instance; + setP5(p5) { + if (isP5Constructor(p5)) { + this.p5Constructor = p5; + this.p5Extensions = p5.prototype; + } else { + console.warn("Invalid p5 object provided to ml5.setP5()."); + } + this.registerPreloads(); } /** + * @internal + * Pass in the ml5 methods which require p5 preload behavior. + * Preload functions must return an object with a property `ready` which is a `Promise`. + * Preloading will be set up immediately if p5 is available on the window. + * Store the references in case p5 is added later. + * + * @param {*} ml5Library - the `ml5` variable. + * @param {Array} methodNames - an array of ml5 functions to preload. + */ + shouldPreload(ml5Library, methodNames) { + this.methodsToPreload = methodNames; + this.ml5Library = ml5Library; + if (this.checkP5()) { + this.registerPreloads(); + } + } + + /** + * @private + * Execute the p5 preload setup using the stored references, provided by shouldPreload(). + * Won't do anything if `shouldPreload()` has not been called or if p5 is not found. + */ + registerPreloads() { + if (this.didSetupPreload) return; + const p5 = this.p5Constructor; + const ml5 = this.ml5Library; + const preloadMethods = this.methodsToPreload; + if (!p5 || !ml5) return; + + // Must shallow copy so that it doesn't reference the replaced method. + const original = { ...ml5 }; + // Must alias `this` so that it can be used inside functions with their own `this` context. + const self = this; + + // Function to be called when a sketch is created, either in global or instance mode. + p5.prototype.myLibInit = function () { + // Bind to this specific p5 instance. + const increment = this._incrementPreload.bind(this); + const decrement = this._decrementPreload.bind(this); + // Replace each preloaded on the ml5 object with a wrapped version which + // increments and decrements the p5 preload counter when called. + preloadMethods.forEach((method) => { + ml5[method] = function (...args) { + increment(); + const result = original[method](...args); + result.ready.then(() => { + decrement(); + }); + return result; + }; + }); + self.didSetupPreload = true; + }; + + // Function to be called when a sketch is destroyed. + p5.prototype.myLibRemove = function () { + // Resets each ml5 method back to its original version. + preloadMethods.forEach((method) => { + ml5[method] = original[method]; + }); + self.didSetupPreload = false; + }; + + p5.prototype.registerMethod("init", p5.prototype.myLibInit); + p5.prototype.registerMethod("remove", p5.prototype.myLibRemove); + } + + /** + * @internal * Dynamic getter checks if p5 is loaded and will return undefined if p5 cannot be found, * or will return an object containing all of the global p5 properties. - * It first checks if p5 is in the window, and then if it is in the p5 property of this.m_p5Instance. - * @returns {p5 | undefined} + * @returns {p5.p5InstanceExtensions | undefined} */ get p5Instance() { - if ( - typeof this.m_p5Instance !== "undefined" && - typeof this.m_p5Instance.loadImage === "function" - ) - return this.m_p5Instance; - - if ( - typeof this.m_p5Instance.p5 !== "undefined" && - typeof this.m_p5Instance.p5.Image !== "undefined" && - typeof this.m_p5Instance.p5.Image === "function" - ) - return this.m_p5Instance.p5; - return undefined; + if (!this.p5Extensions) { + this.findAndSetP5(); + } + return this.p5Extensions; } /**