Handpose

Handpose


Handpose är en maskininlärningsmodell som identifierar olika landmärken (landmarks) i handflata och fingrar. Den kan upptäcka högst en hand åt gången och ger 21 punkter som beskriver viktiga platser på handflatan och fingrarna. Det finns andra modeller som kan identifiera flera händer. Modellen har tränats med många bilder av händer i olika positioner.
De olika landmarksen på handen benämns med tal enligt bild nedan.

image of hand with numbered landmarks

Vi ska skapa ett program som identifierar en hand, ritar ut dess landmarks och mäter distansen mellan toppen på pekfingret och toppen på tummen. Programmet är tänkt att lära ut hur man kan identifiera och använda de olika landmarksen. Med den kunskapen kan man sen bygga många olika program.

Länk till ett program där användaren kan trigga olika animationer för en robot genom att placera fingertoppen på en av de olika fingrarna inom en grön kvadrat. Varje fingertopp triggar sin egen animation. Koden hittar du här. Det programmet kan inte byggas i p5.js editor varför vi väljer att bara länka till det.

Det bibliotek som används kommer från ml5.js och och p5.js.

Kod som står i de ljusgröna fälten nedan är den kod som ska skrivas i editorn. De grå fälten är där kod visas för en närmare beskrivning av delar av koden.

Starta en ny sketch via p5.js editor.

I index.html måste vi lägga till några bibliotek för att få tillgång till ml5.js.
Efter de script som finns lägg till:

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
<script src="https://unpkg.com/ml5@latest/dist/ml5.min.js" type="text/javascript"></script>

Programmet ska ha en rubrik och en canvas som är 640 * 480 pixlar stor .
I body skapas rubriken:

<h1>Handpose with Webcam</h1>

Det finns några variabler som vi ska använda på lite olika platser i koden. För att kunna göra det behöver variablerna vara globala, dvs skapade utanför alla funktioner.
Vi skapar dem med let och ger dem inget initialt värde. Variabeln predictions sätts till en tom array.
handpose är själva modellen. video är den video vi vill visa på canvasen och predictions är en array som fylls med information om den hand som modellen upptäcker

let handpose;
let video;
let predictions = [];

I funktionen setup() skapas en canvas som har bredden 640 pixlar och höjden 480 pixlar. Video skapas med p5.js funktion createCapture(VIDEO) och storleken är samma som canvasens bredd och höjd. video.hide() används för att videon inte ska visas, som ett eget element under canvasen.

Med  handpose = ml5.handpose(video, modelReady); initieras handpose modellen med video och en funktion, modelReady, som ska kallas på när modellen är laddad. Den funktionen finns inte än men ska snart skapas efter setup().

  handpose.on(”predict”, (results) => {
    predictions = results;
  });
säger att när modellen upptäcker en hand ska data för den handen lagras i variabeln predictions som skapades globalt.

Hela koden för funktionen setup:

function setup() {
  createCanvas(640, 480);
  video = createCapture(VIDEO);
  video.size(width, height);
  video.hide();
  handpose = ml5.handpose(video, modelReady);
  handpose.on("predict", (results) => {
    predictions = results;
  });
}

Efter funktionen setup skapar vi funktionen modelReady vilken skriver ut Model Ready! i konsolen när modellen är laddad.

function modelReady() {
  console.log("Model ready!");
}

För att rita ut alla landmarks skapas en funktion som här kallas drawKeypoints. Innan vi skriver den behöver vi lite information om vad modellen returnerar.

Om man gör en console.log() på predictions får man se all information den innehåller. console.log(predictions) kan skrivas i funktionen setup direkt efter koden predictions = results;
Om du gör det stoppa programmet direkt när du fått ett resultat. Det blir enormt många loggar och det finns risk för att datorn låser sig.

Det som lagras i predictions är ett objekt som ser ut så här:

  1. (1) [Object]
    1. ▶0: Object
      1. handInViewConfidence: 0.9998456239700317
      2. ▶boundingBox: Object
      3. ▶landmarks: Array(21)
      4. ▶annotations: Object

handInViewConfidence: talar om med vilken säkerhet modellen tror at det är en hand i bild

boundingBox: innehåller information om var i videon modellen anser att handen befinner sig

landmarks: är en array av alla de 21 landmarks som modellen identifierar

annotations: är ett objekt som innehäller information om varje finger och handflatan

Det är de olika landmarksen som vi här behöver identifiera för att kunna rita ut cirklar på dem. I varje landmark finns information om dess x, y och z position. Vi kommer inte att använda z-positionen då canvasen bara visar 2 dimensioner.

Vi behöver alltså loopa igenom hela predictions och därefter alla landmarks. Detta görs med två for-loopar, en yttre loop där varje predictions sparas namnet prediction och en inre loop. där varje landmark sparas med namnet keypoint. Om du behöver uppdatera dig på for-loop kan du titta här.

Looparna ser ut så här:

for (let i = 0; i < predictions.length; i++) {
    const prediction = predictions[i];
    for (let j = 0; j < prediction.landmarks.length; j++) {
      const keypoint = prediction.landmarks[j];
    }
}

Varje keypoint har tre värden. keypoint[0] är dess x-position och keypoint[1] är dess y-position.

Vi kan då rita ut en grön ellipse som har diametern 10 på varje keypoint. Den koden ska skrivas innan de två avslutande krullparenteserna ( } ) i koden ovan.

fill(0, 255, 0);
noStroke();
ellipse(keypoint[0], keypoint[1], 10, 10);

För att beräkna distansen mellan toppen på pekfingret och toppen på tummen kan vi använda en funktion från p5.js som heter dist(). Syntax för dist är dist(x1, y1, x2, y2). Den returnerar ett tal med distansen mellan två punkter. Då den returnerar ett decimaltal behöver vi använda en annan p5.js funktion som heter round() vilken avrundar uppåt till närmaset heltal. Vi sparar värdet i en variabel med namnet distance. landmarks[4][0] är tummens tops x -position, landmarks[4][1] är tummens tops y-position, landmarks[8][0] är pekfingrets tops x-position och landmarks[8][1] är pekfingrets tops
y-position. Även den här koden ska placeras före de två de två avslutande krullparenteserna ( } ) i koden för for-loopar.

  let distance = round(
        dist(
          prediction.landmarks[4][0],
          prediction.landmarks[4][1],
          prediction.landmarks[8][0],
          prediction.landmarks[8][1]
        )
  );

När vi nu har de värdena kan vi rita text på canvasen. Texten ska vara vit, storleken 32 pixlar och det ska stå Distance: värdet på distansen och texten ska skrivas ut på canvasen med början i x = 10 och y = 50.
Även den här koden ska placeras före de två de två avslutande krullparenteserna ( } ) i koden för for-loopar.

fill(255);
textSize(32);
text("Distance: " + distance, 10, 50);

Hela koden för funktionen drawKeypoints:

function drawKeypoints() {
  for (let i = 0; i < predictions.length; i++) {
    const prediction = predictions[i];
    for (let j = 0; j < prediction.landmarks.length; j++) {
      const keypoint = prediction.landmarks[j];
      fill(0, 255, 0);
      noStroke();
      ellipse(keypoint[0], keypoint[1], 10, 10);
      let distance = round(
        dist(
          prediction.landmarks[4][0],
          prediction.landmarks[4][1],
          prediction.landmarks[8][0],
          prediction.landmarks[8][1]
        )
      );
      fill(255);
      textSize(32);
      text("Distance: " + distance, 10, 50);
    }
  }
}

I funktionen draw() kallar vi på funktionen drawKeypoints och sätter bakgrunden till svart. Funktionen draw uppdateras 60 gånger per sekund vilket gör att vi kan se handens rörelse.

Hela koden för funktionen draw:

function draw() {
  background(0);
  drawKeypoints();
}

Hela koden för programmet finns att se här.