0t1 steckt noch in den Kinderschuhen.

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.

Holzgestell

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


© 0t1

Cookies

0t1 mag keine Kekse (und kein Tracking). Wir verwenden lediglich notwendige Cookies für essentielle Funktionen.

Wir verwenden Schriftarten von Adobe Fonts. Dafür stellt dein Browser eine Verbindung zu den Servern von Adobe in den USA her. Wenn du unsere Seite nutzen möchtest, musst du dich damit einverstanden erklären.

Weitere Informationen in unserer Datenschutzerklärung.