feat: add hand tracking and painting functionality using ml5.js handPose model
- Implemented hand tracking with real-time video capture - Added GUI controls for canvas background, keypoints, connections, and hand settings - Enabled painting with hand gestures, including pinch to draw and clear gesture - Configured rendering options for keypoints and connections - Introduced bounds visualization for detected hands
This commit is contained in:
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Make default camera /dev/video0 point to the "best" camera present.
|
||||||
|
|
||||||
|
if [ -h /dev/video0 ]; then
|
||||||
|
sudo rm /dev/video0 # not first run: remove our old symlink
|
||||||
|
elif [ -e /dev/video0 ]; then
|
||||||
|
sudo mv /dev/video0 /dev/video0.original # first run: rename original video0
|
||||||
|
fi
|
||||||
|
if [ -e /dev/video1 ]; then
|
||||||
|
sudo ln -s /dev/video1 /dev/video0 # symlink to video1 since it exists
|
||||||
|
echo "Set default camera /dev/video0 --> external camera /dev/video1"
|
||||||
|
elif [ -e /dev/video0.original ]; then # symlink to video0.original otherwise
|
||||||
|
sudo ln -s /dev/video0.original /dev/video0
|
||||||
|
echo "Set default camera /dev/video0 --> integrated camera /dev/video0.original"
|
||||||
|
else
|
||||||
|
echo "Sorry, does this machine have no camera devices?"
|
||||||
|
ls -l /dev/video*
|
||||||
|
fi
|
||||||
|
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>handtracking</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #1d1d1d;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-color: #f00;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="./libraries/p5.min.js"></script>
|
||||||
|
<script src="./libraries/ml5.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.20"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="sketch.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Vendored
+23069
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -0,0 +1,322 @@
|
|||||||
|
let video;
|
||||||
|
let handPose;
|
||||||
|
let hands = [];
|
||||||
|
let gui = new lil.GUI();
|
||||||
|
let folders = {};
|
||||||
|
let painting;
|
||||||
|
let connections = [
|
||||||
|
[0, 1],
|
||||||
|
[1, 2],
|
||||||
|
[2, 3],
|
||||||
|
[3, 4],
|
||||||
|
[1, 5],
|
||||||
|
[5, 6],
|
||||||
|
[6, 7],
|
||||||
|
[7, 8],
|
||||||
|
[13, 9],
|
||||||
|
[9, 5],
|
||||||
|
[2, 5],
|
||||||
|
[9, 10],
|
||||||
|
[10, 11],
|
||||||
|
[11, 12],
|
||||||
|
[17, 13],
|
||||||
|
[13, 14],
|
||||||
|
[14, 15],
|
||||||
|
[15, 16],
|
||||||
|
[0, 17],
|
||||||
|
[17, 18],
|
||||||
|
[18, 19],
|
||||||
|
[19, 20]
|
||||||
|
];
|
||||||
|
let clearStart = 0;
|
||||||
|
let videoScale;
|
||||||
|
let prev = {
|
||||||
|
Left: { x: null, y: null },
|
||||||
|
Right: { x: null, y: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
let conf = {
|
||||||
|
hands: {
|
||||||
|
palm_kps: [0, 1, 5, 9, 13, 17],
|
||||||
|
palm_kps_str: null,
|
||||||
|
Left: "#ff0000",
|
||||||
|
Right: "#0000ff",
|
||||||
|
show: true,
|
||||||
|
paint: true,
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
bg: "#00000000"
|
||||||
|
},
|
||||||
|
kps: {
|
||||||
|
size: 8,
|
||||||
|
color: "#000000",
|
||||||
|
draw: true,
|
||||||
|
},
|
||||||
|
kp_conn: {
|
||||||
|
connect: true,
|
||||||
|
stroke: 1,
|
||||||
|
},
|
||||||
|
kp_text: {
|
||||||
|
show: false,
|
||||||
|
size: 8,
|
||||||
|
offx: 10,
|
||||||
|
offy: 5,
|
||||||
|
color: "#000000ff"
|
||||||
|
},
|
||||||
|
camera: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
hand_bounds: {
|
||||||
|
show: false,
|
||||||
|
stroke: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function preload() {
|
||||||
|
handPose = ml5.handPose({ flipped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function guiInit() {
|
||||||
|
conf.hands.palm_kps_str = conf.hands.palm_kps.join(", ");
|
||||||
|
|
||||||
|
folders.canvas = gui.addFolder("Canvas");
|
||||||
|
{
|
||||||
|
folders.canvas.addColor(conf.canvas, "bg");
|
||||||
|
folders.canvas.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.render = gui.addFolder("Rendering");
|
||||||
|
{
|
||||||
|
|
||||||
|
folders.render.points = folders.render.addFolder("Points");
|
||||||
|
{
|
||||||
|
folders.render.points.add(conf.kps, "draw");
|
||||||
|
folders.render.points.addColor(conf.kps, "color");
|
||||||
|
folders.render.points.add(conf.kps, "size", 1, 30, 0.5);
|
||||||
|
folders.render.points.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.render.conn = folders.render.addFolder("Connections");
|
||||||
|
{
|
||||||
|
folders.render.conn.add(conf.kp_conn, "connect");
|
||||||
|
folders.render.conn.add(conf.kp_conn, "stroke", 0.5, 10, 0.5);
|
||||||
|
folders.render.conn.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.render.labels = folders.render.addFolder("Labels");
|
||||||
|
{
|
||||||
|
folders.render.labels.add(conf.kp_text, "show");
|
||||||
|
folders.render.labels.add(conf.kp_text, "size", 4, 32, 1);
|
||||||
|
folders.render.labels.add(conf.kp_text, "offx", -50, 50, 1);
|
||||||
|
folders.render.labels.add(conf.kp_text, "offy", -50, 50, 1);
|
||||||
|
folders.render.labels.addColor(conf.kp_text, "color");
|
||||||
|
folders.render.labels.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.view = gui.addFolder("View");
|
||||||
|
{
|
||||||
|
|
||||||
|
folders.view.camera = folders.view.addFolder("Camera");
|
||||||
|
{
|
||||||
|
folders.view.camera.add(conf.camera, "show");
|
||||||
|
folders.view.camera.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.view.bounds = folders.view.addFolder("Bounds");
|
||||||
|
{
|
||||||
|
folders.view.bounds.add(conf.hand_bounds, "show");
|
||||||
|
folders.view.bounds.add(conf.hand_bounds, "stroke", 0.5, 10, 0.5);
|
||||||
|
folders.view.bounds.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.view.hands = folders.view.addFolder("Hands");
|
||||||
|
{
|
||||||
|
folders.view.hands.add(conf.hands, "palm_kps_str").name("palm_kps").onChange((value) => {
|
||||||
|
conf.hands.palm_kps = value.trim().split(",").map(Number).map(i => isNaN(i) ? 0 : clamp(0, i, 20));
|
||||||
|
});
|
||||||
|
folders.view.hands.addColor(conf.hands, "Left");
|
||||||
|
folders.view.hands.addColor(conf.hands, "Right");
|
||||||
|
folders.view.hands.add(conf.hands, "show");
|
||||||
|
folders.view.hands.add(conf.hands, "paint");
|
||||||
|
folders.view.hands.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
guiInit();
|
||||||
|
createCanvas(window.innerWidth, window.innerHeight);
|
||||||
|
painting = createGraphics(width, height);
|
||||||
|
video = createCapture({
|
||||||
|
video: {
|
||||||
|
},
|
||||||
|
audio: false,
|
||||||
|
flipped: true,
|
||||||
|
});
|
||||||
|
videoScale = Math.min(window.innerWidth / 640, window.innerHeight / 480);
|
||||||
|
video.size(640 * videoScale, 480 * videoScale);
|
||||||
|
handPose.detectStart(video, (results) => { hands = results; });
|
||||||
|
|
||||||
|
video.hide();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
clear();
|
||||||
|
strokeWeight(1);
|
||||||
|
stroke(255);
|
||||||
|
fill(conf.canvas.bg);
|
||||||
|
rect(0, 0, width, height);
|
||||||
|
rect(0, 0, video.width, video.height);
|
||||||
|
|
||||||
|
if (conf.camera.show) {
|
||||||
|
image(video, 0, 0);
|
||||||
|
}
|
||||||
|
if (conf.hands.paint) {
|
||||||
|
image(painting, 0, 0);
|
||||||
|
} else {
|
||||||
|
painting.clear();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < hands.length; i++) {
|
||||||
|
let hand = hands[i];
|
||||||
|
let bounds = { min: { x: Infinity, y: Infinity }, max: { x: -Infinity, y: -Infinity } }
|
||||||
|
|
||||||
|
if (conf.kp_conn.connect) {
|
||||||
|
drawConnections(hand);
|
||||||
|
}
|
||||||
|
|
||||||
|
loopKP(hand, i, bounds);
|
||||||
|
if (conf.hands.show) drawHandText(hand, i);
|
||||||
|
if (conf.hand_bounds.show) drawBounds(bounds, hand.handedness);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawConnections(hand) {
|
||||||
|
for (let j = 0; j < connections.length; j++) {
|
||||||
|
let pointAIndex = connections[j][0];
|
||||||
|
let pointBIndex = connections[j][1];
|
||||||
|
let pointA = hand.keypoints[pointAIndex];
|
||||||
|
let pointB = hand.keypoints[pointBIndex];
|
||||||
|
stroke(conf.hands[hand.handedness]);
|
||||||
|
strokeWeight(conf.kp_conn.stroke);
|
||||||
|
line(pointA.x, pointA.y, pointB.x, pointB.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBounds(bounds, handedness) {
|
||||||
|
let x = bounds.min.x;
|
||||||
|
let y = bounds.min.y;
|
||||||
|
let w = bounds.max.x - x;
|
||||||
|
let h = bounds.max.y - y;
|
||||||
|
noFill();
|
||||||
|
stroke(conf.hands[handedness]);
|
||||||
|
strokeWeight(conf.hand_bounds.stroke);
|
||||||
|
rect(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loopKP(hand, i, bounds) {
|
||||||
|
for (let j = 0; j < hand.keypoints.length; j++) {
|
||||||
|
const kp = hand.keypoints[j];
|
||||||
|
fill(conf.kps.color);
|
||||||
|
noStroke();
|
||||||
|
if (conf.kps.draw) circle(kp.x, kp.y, conf.kps.size);
|
||||||
|
|
||||||
|
if (kp.x < bounds.min.x) bounds.min.x = kp.x
|
||||||
|
if (kp.y < bounds.min.y) bounds.min.y = kp.y
|
||||||
|
if (kp.x > bounds.max.x) bounds.max.x = kp.x
|
||||||
|
if (kp.y > bounds.max.y) bounds.max.y = kp.y
|
||||||
|
|
||||||
|
if (conf.kp_text.show) {
|
||||||
|
fill(conf.kp_text.color);
|
||||||
|
textSize(conf.kp_text.size);
|
||||||
|
text(`[${i}][${j}]`, kp.x + conf.kp_text.offx, kp.y - conf.kp_text.offy);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doPainting(hand);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawHandText(hand, i) {
|
||||||
|
fill(conf.hands[hand.handedness]);
|
||||||
|
textSize(conf.kp_text.size * 1.5);
|
||||||
|
|
||||||
|
let avgX = conf.hands.palm_kps.reduce((sum, i) => sum + hand.keypoints[i].x, 0) / conf.hands.palm_kps.length;
|
||||||
|
let avgY = conf.hands.palm_kps.reduce((sum, i) => sum + hand.keypoints[i].y, 0) / conf.hands.palm_kps.length;
|
||||||
|
let offset = (conf.kp_text.size * 1.5) / 2;
|
||||||
|
text(hand.handedness, avgX - textWidth(hand.handedness) / 2, avgY + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doPainting(hand) {
|
||||||
|
const thumbTip = hand.keypoints[4];
|
||||||
|
const indexTip = hand.keypoints[8];
|
||||||
|
const handColor = conf.hands[hand.handedness];
|
||||||
|
const prevPoint = prev[hand.handedness];
|
||||||
|
|
||||||
|
const getMidpoint = (a, b) => ({
|
||||||
|
x: (a.x + b.x) / 2,
|
||||||
|
y: (a.y + b.y) / 2
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetPrev = () => {
|
||||||
|
prevPoint.x = null;
|
||||||
|
prevPoint.y = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawPaintStroke = (mid) => {
|
||||||
|
fill("#00ff00");
|
||||||
|
noStroke();
|
||||||
|
|
||||||
|
painting.noStroke();
|
||||||
|
painting.fill(handColor);
|
||||||
|
circle(thumbTip.x, thumbTip.y, conf.kps.size * 1.5);
|
||||||
|
circle(indexTip.x, indexTip.y, conf.kps.size * 1.5);
|
||||||
|
|
||||||
|
painting.circle(mid.x, mid.y, conf.kps.size * 1.5);
|
||||||
|
painting.stroke(handColor);
|
||||||
|
painting.strokeWeight(conf.kps.size * 1.5);
|
||||||
|
painting.line(prevPoint.x ?? mid.x, prevPoint.y ?? mid.y, mid.x, mid.y);
|
||||||
|
|
||||||
|
prevPoint.x = mid.x;
|
||||||
|
prevPoint.y = mid.y;
|
||||||
|
};
|
||||||
|
|
||||||
|
let pinchClose = dist(thumbTip.x, thumbTip.y, indexTip.x, indexTip.y) < 30;
|
||||||
|
let pinchFar = dist(thumbTip.x, thumbTip.y, indexTip.x, indexTip.y) > 60;
|
||||||
|
if (pinchClose && conf.hands.paint) {
|
||||||
|
drawPaintStroke(getMidpoint(thumbTip, indexTip));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pinchFar) {
|
||||||
|
resetPrev();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hands.length >= 2) {
|
||||||
|
const [h1, h2] = hands;
|
||||||
|
const clearGesture = dist(h1.index_finger_tip.x, h1.index_finger_tip.y, h2.index_finger_tip.x, h2.index_finger_tip.y) < 20;
|
||||||
|
if (clearGesture) {
|
||||||
|
if (clearStart === 0) {
|
||||||
|
clearStart = Date.now();
|
||||||
|
}
|
||||||
|
fill("#ff0000");
|
||||||
|
noStroke();
|
||||||
|
circle((h1.index_finger_tip.x + h2.index_finger_tip.x) / 2, (h1.index_finger_tip.y + h2.index_finger_tip.y) / 2, 40);
|
||||||
|
if (Date.now() - clearStart > 200) {
|
||||||
|
painting.clear();
|
||||||
|
resetPrev();
|
||||||
|
pinchClose = false;
|
||||||
|
pinchFar = true;
|
||||||
|
clearStart = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
clearStart = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = (min, num, max) => Math.min(Math.max(num, min), max);
|
||||||
Reference in New Issue
Block a user