IxD2 | airband
In diesem Projekt lernt man, wie man mit Hilfe von p5.js, einer Webcam und den eigenen Händen musizieren kann (beispielsweise Gitarre spielen).
Die Idee
Unsere Idee ist es zwei interaktive Stationen zu kostruieren, bei denen man durch Handbewegungen verschiedene Instrumente spielen kann.
Wir haben uns dazu entschieden bei Station 1 ein Schlagzeug zu nehmen, da dies in unserer Anwendung wahrscheinlich das am einfachsten zu bedienenste Instrument ist. So können die Besucher die Technik kennenlernen und sich im Anschluss in Station 2 am Gitarrespielen versuchen.
Umsetzung
Hardware
Umgesetzt haben wir das Ganze, indem wir aus mehreren Holzbrettern einen tiefen Rahmen gebaut haben. Im Anschluss wurde dieser von beiden Seiten mit einem grauen Holzbrett verschlossen, wobei jeweils auf beiden Seiten ein Loch gelassen wurde, um einen IMac darin zu platzieren. Diese IMacs sind gleichzeitig jeweils die Webcam, als auch der Monitor. Die Kamera des IMacs erfasst die Bewegungen der Hände und spiegelt sie in Form von grünen Punkten auf dem Monitor wieder. Auf diese Art und Weise sieht der Benutzer wo sich seine Hände befinden und er kann den Anweisungen folgen.

Software
Der HTML-Code Station 1 und 2
Zu Beginn des Projektes erstellen wir eine HTML-Datei mit den benötigten Soundlinks. Ebenfalls wird auch die Datei «sketch.js» eingebunden.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ml5.js handPose Webcam Example</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/addons/p5.sound.min.js"></script>
<script src="https://unpkg.com/ml5@1.0.1/dist/ml5.min.js"></script>
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
sketch.js Station 1
Globale Variablen
let handPose;
let video;
let hands = [];
let sounds = [];
let handStates = []; // Array to keep track of hand states (closed or open)
handpose
: wird verwendet, um eine Instanz des Handpose-Modells zu speichern. Das Handpose-Modell wird verwendet, um die Positionen der Hände in unserem Video zu erfassen.
video
: Diese Variable speichert die Erfassung des Videostreams, der in Echtzeit von der Webcam des Benutzers stammt. Das Video wird verwendet, um die Handposenerkennung durchzuführen.
hands
: Speichert die Position der Hand als Variable (Bei Station 2 mit grünen Punkten zu erkennen)
sounds
: wird verwendet um die .mp3s der Schlagzeuggeräusche mit dem Code zu verbinden.
handStates
: Ist eine Variable um mithilfe eines Arrays zu erfassen ob die Hand geöffnet oder geschlossen ist.
Laden der Soundfiles mit preload
function preload() {
// Load the handPose model
handPose = ml5.handPose();
// Load the sound files
sounds[0] = loadSound('ton_1.mp3');
sounds[1] = loadSound('ton_2.mp3');
sounds[2] = loadSound('ton_3.mp3');
sounds[3] = loadSound('ton_4.mp3');
sounds[4] = loadSound('ton_5.mp3');
sounds[5] = loadSound('ton_6.mp3');
sounds[6] = loadSound('ton_7.mp3');
sounds[7] = loadSound('ton_8.mp3');
sounds[8] = loadSound('ton_9.mp3');
}
Einbinden der Webcam
function setup() {
createCanvas(640, 480);
// Create the webcam video and hide it
video = createCapture(VIDEO);
video.size(640, 480);
video.hide();
// Start detecting hands from the webcam video
handPose.detectStart(video, gotHands);
}
Durch die Funktion setup
wird ein Canvas in einer gewünschten Größe erzeugt und anschließend versteckt. handPose.detectStart
sorgt dann dafür, das die Webcam beginnt die Hände zu tracken.
Das Visuelle
function draw() {
// Draw the webcam video
// Push and pop ensure that transformations do not affect other parts of the code
push();
translate(width, 0); // Move the coordinate system to the right edge of the canvas
scale(-1, 1); // Flip the coordinate system horizontally
image(video, 0, 0, width, height); // Draw the video
pop();
// Draw the grid lines
drawGrid();
// Draw all the tracked hand points
for (let i = 0; i < hands.length; i++) {
let hand = hands[i];
Das Visuelle, sprich das Kamerabild, sowie das grid, werden mit der Funktion draw
erstellt und anschließend nach belieben definiert. Außerdem zeichnet die for-Schleife alle getrackten Punkte in grün.
Checkconditions und Callbackfunktion
// Check if the hand is closed
let isClosed = isHandClosed(hand);
if (isClosed && !handStates[i]) {
// Hand just closed
let gridIndex = getGridIndex(hand);
if (gridIndex !== -1) {
console.log(`Hand closed detected in grid ${gridIndex}. Playing sound.`);
sounds[gridIndex].play();
}
}
// Update hand state
handStates[i] = isClosed;
}
}
// Callback function for when handPose outputs data
function gotHands(results) {
// Save the output to the hands variable
hands = results;
// Initialize hand states if needed
while (handStates.length < hands.length) {
handStates.push(false);
}
}
Die Checkconditions bewirken, dass ein Ton erzeugt wird, sobald die Hand/Hände geschlossen ist/sind.
Die Callbackfunktion resettet das Ganze.
// Function to determine if a hand is closed
function isHandClosed(hand) {
// Use the keypoints for fingertips and palm base
let tips = [8, 12, 16, 20]; // Indices for fingertips in hand landmarks
let palmBase = hand.keypoints[0]; // Wrist point
// Check if all fingertips are close to the palm base
for (let i = 0; i < tips.length; i++) {
let tip = hand.keypoints[tips[i]];
let distance = dist(tip.x, tip.y, palmBase.x, palmBase.y);
if (distance > 50) { // Threshold distance to consider the hand closed
return false;
}
}
return true;
}
// Function to draw the 3x3 grid
function drawGrid() {
stroke(255, 0, 0); // Set the color of the grid lines
strokeWeight(2); // Set the thickness of the grid lines
// Draw vertical lines
line(width / 3, 0, width / 3, height);
line(2 * width / 3, 0, 2 * width / 3, height);
// Draw horizontal lines
line(0, height / 3, width, height / 3);
line(0, 2 * height / 3, width, 2 * height / 3);
// Draw grid numbers
textSize(32);
textAlign(CENTER, CENTER);
text('1', width / 6, height / 6);
text('2', width / 2, height / 6);
text('3', 5 * width / 6, height / 6);
text('4', width / 6, height / 2);
text('5', width / 2, height / 2);
text('6', 5 * width / 6, height / 2);
text('7', width / 6, 5 * height / 6);
text('8', width / 2, 5 * height / 6);
text('9', 5 * width / 6, 5 * height / 6);
fill(0, 255, 0); // Textfarbe
}
// Function to get the grid index based on the hand position
function getGridIndex(hand) {
let x = width - hand.keypoints[0].x; // Mirror the x-coordinate
let y = hand.keypoints[0].y;
if (x < width / 3 && y < height / 3) return 0; // Top left
if (x < 2 * width / 3 && y < height / 3) return 1; // Top middle
if (x < width && y < height / 3) return 2; // Top right
if (x < width / 3 && y < 2 * height / 3) return 3; // Middle left
if (x < 2 * width / 3 && y < 2 * height / 3) return 4; // Middle middle
if (x < width && y < 2 * height / 3) return 5; // Middle right
if (x < width / 3 && y < height) return 6; // Bottom left
if (x < 2 * width / 3 && y < height) return 7; // Bottom middle
if (x < width && y < height) return 8; // Bottom right
return -1; // Not in any grid
}
Zu guter Letzt prüft man mit Keypoints ob die jeweilige Hand geschlossen oder geöffnet ist. Außerdem zeichnet die Funktion draw mit roten Linien, das vorher erstellte Grid nach und füllt die 9 entstandenen Quadrate mit Zahlen.

So sieht der Code dann in Action aus.

sketch.js Station 2
Globalevariablen
let handPose;
let video;
let hands = [];
let eMollSound, cDurSound, gDurSound, dDurSound;
let wasAboveMidline = true;
Die ersten drei Variablen sind schon bekannt aus Station 1.
eMollSound
, cDurSound
, gDurSound
, dDurSound
: werden verwendet, um die mp3-Dateien der Töne mit dem Code zu verbinden.
wasAboveMidline
: Mit dieser Variable wird dafür gesorgt, das ein Ton erneut gespielt wird, sobald die Hand einmal über der horizontalen Mittelachse erfasst wird.
function preload() {
// Load the handPose model and the sound files
handPose = ml5.handPose();
eMollSound = loadSound('e_moll.mp3');
cDurSound = loadSound('c_dur.mp3');
gDurSound = loadSound('g_dur.mp3');
dDurSound = loadSound('d_dur.mp3');
}
Laden der Soundfiles mit preload
Einbinden der Webcam
function setup() {
createCanvas(640, 480);
// Create the webcam video and hide it
video = createCapture(VIDEO);
video.size(640, 480);
video.hide();
// Start detecting hands from the webcam video
handPose.detectStart(video, gotHands);
}
Hier ändert sich nichts im Vergleich zu Station 1.
Das Visuelle
function draw() {
// Draw the webcam video
translate(width, 0); // Move to the right edge
scale(-1, 1); // Flip the x-axis to mirror the video
image(video, 0, 0, width, height);
// Draw vertical lines to divide the video into 6 columns
let numColumns = 6;
let colWidth = width / numColumns;
stroke(255, 0, 0);
for (let i = 1; i < numColumns; i++) {
line(i * colWidth, 0, i * colWidth, height);
}
// Draw column numbers/letters
textSize(24);
fill(255, 0, 0);
let labels = ["slow", "fast", "G", "C", "D", "Em"];
for (let i = 0; i < numColumns; i++) {
textAlign(CENTER, TOP);
push();
translate(i * colWidth + colWidth / 2, 10);
scale(-1, 1); // Flip the text horizontally
text(labels[i], 0, 0);
pop();
}
Das Visuelle, sprich das Kamerabild, sowie die Roten Linien und Zahlen, werden mit der Funktion draw
erstellt und anschließend nach belieben definiert.
Das Handtracking
// Track the positions of both hands
let rightHandColumn = -1;
let leftHandY = -1;
for (let i = 0; i < hands.length; i++) {
let hand = hands[i];
let keypoint9 = hand.keypoints[9];
fill(0, 255, 0);
noStroke();
circle(keypoint9.x, keypoint9.y, 10);
if (hand.handedness === "Right") {
rightHandColumn = Math.floor(keypoint9.x / colWidth) + 1;
}
if (hand.handedness === "Left") {
leftHandY = keypoint9.y;
}
}
Hier wird die Bewegung der Hand des Besuchers getrackt. Anhand von keypoints
werden grüne Punkte auf den Händen im Kamerabild erzeugt. Durch diese weiß der Benutzer wo seine Hände getrackt werden und kann seine Handlungen anpassen.
Checkconditions und Callbackfunktion
// Check conditions for playing sound
if (leftHandY > height / 2 && wasAboveMidline) {
if (rightHandColumn === 6) {
eMollSound.play();
} else if (rightHandColumn === 5) {
dDurSound.play();
} else if (rightHandColumn === 4) {
cDurSound.play();
} else if (rightHandColumn === 3) {
gDurSound.play();
} else if (rightHandColumn === 2) {
// Play a sound when the left hand moves up into the "fast" column
gDurSound.play();
}
wasAboveMidline = false;
} else if (leftHandY <= height / 2) {
wasAboveMidline = true;
}
}
// Callback function for when handPose outputs data
function gotHands(results) {
// Save the output to the hands variable
hands = results;
}
Die Checkconditions sorgen mit einem if-else
Statement dafür, das man generell einen Ton hören kann, sobald die getrackte Hand sich von oben durch die Bildmittellinie bewegt. Ebenfalls wird durch sie festgelegt, welcher Ton hörbar sein soll. Je nach Spalte, in der sich die Hand befindet ertönt ein anderer Ton. Zusätzlich wird in der Spalte "fast" außerdem ein erneuter Ton erzeugt, wenn die Hand in Spalte 2 von unten nach oben durch die Bildmittellinie bewegt wird.
Die Callbackfunktion bewirkt, dass sich die Funktion gotHands
resettet.
So sieht der Code dann in Action aus.

Der Prototyp
Am Ende sieht der fertige Prototyp folgendermaßen aus.

Schlussfolgerung / Fazit
Innerhalb dieses Projektes haben wir unser vorher erlerntes Wissen, welches wir aus dem Tutorial gezogen haben vertieft und haben uns noch mehr mit dem Thema Handtracking beschäftigt. Unser Ziel, eine "air band" zu erschaffen ist uns gelungen und wir haben es geschafft mit Hilfe von p5.js und ml5.js ein Musikinstrument (beispielsweise die Gitarre) ohne eigentliches Instrument zu spielen. Außerdem haben wir es sehr gut umgesetzt bekommen die Arbeit untereinander aufzuteilen, und so noch effektiver arbeiten zu können. Zusätzlich haben wir uns immer wieder abgesprochen und den aktuellen Stand verglichen um durch mehrere Lösungswege einen besten kombinierten Weg zu bekommen.
Links
https://editor.p5js.org/Anfaengerlama/sketches/VeIvbspdF
https://editor.p5js.org/Anfaengerlama/sketches/VlcsM4SBq
Quellen
Tracken der Hand: https://docs.ml5js.org/#/reference/handpose