Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Interactive bodyPose example #87

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/Bodypose-single-image/data/runner.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions examples/Bodypose-single-image/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
Copyright (c) 2024 ml5

This software is released under the MIT License.
https://opensource.org/licenses/MIT
-->

<html>
<head>
<meta charset="UTF-8" />
<title>ml5.js BodyPose Detection Example</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>
<script src="../../dist/ml5.js"></script>
</head>

<body>
<h1>BodyPose example on image with single-person detection</h1>
<p id="status">Loading model...</p>
<form>
<label>
<input type="checkbox" id="skeleton" checked/>
Skeleton
</label>
<label>
<input type="checkbox" id="keypoints" checked/>
Keypoints
</label>
<label>
<input type="checkbox" id="box"/>
Bounding Box
</label>
<label>
<input type="checkbox" id="labels"/>
Part Labels
</label>
</form>
<img id="image" style="display:none" src="data/runner.jpg" alt="runner image" />
<canvas id="canvas"></canvas>
<p>image by Funk Dooby via <a href="https://commons.wikimedia.org/wiki/File:Sandwich_10K_2019_FNK_3606_(48120497711).jpg" target="_blank">Wikimedia</a></p>
<script src="sketch.js"></script>
</body>
</html>
111 changes: 111 additions & 0 deletions examples/Bodypose-single-image/sketch.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
});
});
}
20 changes: 0 additions & 20 deletions src/BodyPose/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
21 changes: 0 additions & 21 deletions src/BodySegmentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -116,9 +113,6 @@ class BodySegmentation {
modelConfig
);

// for compatibility with p5's preload()
if (this.p5PreLoadExists) window._decrementPreload();

return this;
}
/**
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
21 changes: 0 additions & 21 deletions src/FaceMesh/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -77,9 +74,6 @@ class FaceMesh {
modelConfig
);

// for compatibility with p5's preload()
if (this.p5PreLoadExists) window._decrementPreload();

return this;
}

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
20 changes: 0 additions & 20 deletions src/HandPose/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/NeuralNetwork/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Sentiment/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Sentiment>}
* @public
*/
this.ready = callCallback(this.loadModel(modelName), callback);
Expand Down
31 changes: 15 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading